From bcb2e73e5383ae4ae34a1c65439a566b87ca638c Mon Sep 17 00:00:00 2001 From: Vasily Styagov Date: Fri, 4 Oct 2024 19:22:07 +0100 Subject: [PATCH 01/93] move interest calculation to terms --- contract/src/jar/account/mod.rs | 1 + contract/src/jar/account/v2.rs | 20 ++ contract/src/jar/model/mod.rs | 2 + contract/src/jar/model/v2.rs | 72 +++++ contract/src/product/model/mod.rs | 2 + .../src/product/{model.rs => model/v1.rs} | 0 contract/src/product/model/v2.rs | 274 ++++++++++++++++++ 7 files changed, 371 insertions(+) create mode 100644 contract/src/jar/account/v2.rs create mode 100644 contract/src/jar/model/v2.rs create mode 100644 contract/src/product/model/mod.rs rename contract/src/product/{model.rs => model/v1.rs} (100%) create mode 100644 contract/src/product/model/v2.rs diff --git a/contract/src/jar/account/mod.rs b/contract/src/jar/account/mod.rs index b18c2457..2671e03e 100644 --- a/contract/src/jar/account/mod.rs +++ b/contract/src/jar/account/mod.rs @@ -1,4 +1,5 @@ pub mod v1; +pub mod v2; pub mod versioned; pub type AccountJarsLastVersion = v1::AccountV1; diff --git a/contract/src/jar/account/v2.rs b/contract/src/jar/account/v2.rs new file mode 100644 index 00000000..a3b47b90 --- /dev/null +++ b/contract/src/jar/account/v2.rs @@ -0,0 +1,20 @@ +use std::ops::{Deref, DerefMut}; + +use near_sdk::near; +use sweat_jar_model::jar::JarId; + +use crate::{ + jar::model::{AccountJarsLegacy, Jar}, + migration::account_jars_non_versioned::AccountJarsNonVersioned, + score::AccountScore, +}; + +#[near] +#[derive(Default, Debug, PartialEq)] +pub struct AccountV2 { + /// Is used as nonce in `get_ticket_hash` method. + pub nonce: u32, + pub jars: Vec, + pub score: AccountScore, + pub is_penalty_applied: bool, +} diff --git a/contract/src/jar/model/mod.rs b/contract/src/jar/model/mod.rs index 3c12e57d..ed6f767d 100644 --- a/contract/src/jar/model/mod.rs +++ b/contract/src/jar/model/mod.rs @@ -1,10 +1,12 @@ mod common; mod legacy; mod v1; +mod v2; mod versioned; pub use common::{JarCache, JarTicket}; pub use legacy::*; +pub use v2::*; pub use versioned::Jar; pub type JarLastVersion = v1::JarV1; diff --git a/contract/src/jar/model/v2.rs b/contract/src/jar/model/v2.rs new file mode 100644 index 00000000..c28a30f3 --- /dev/null +++ b/contract/src/jar/model/v2.rs @@ -0,0 +1,72 @@ +use near_sdk::{near, AccountId}; +use sweat_jar_model::{jar::JarId, ProductId, TokenAmount, UDecimal, MS_IN_YEAR}; + +use crate::{common::Timestamp, jar::model::JarCache}; + +/// The `Jar` struct represents a deposit jar within the smart contract. +#[near(serializers=[borsh, json])] +#[derive(Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)] +pub struct JarV2 { + pub deposits: Vec, + pub cache: Option, + pub claimed_balance: TokenAmount, + pub is_pending_withdraw: bool, + pub claim_remainder: u64, +} + +#[near(serializers=[borsh, json])] +#[derive(Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)] +pub struct Deposit { + pub created_at: Timestamp, + pub principal: TokenAmount, +} + +impl Deposit { + fn get_interest_with_apy( + &self, + apy: UDecimal, + product: &Product, + now: Timestamp, + since_date: Option, + ) -> (TokenAmount, u64) { + let since_date = since_date.unwrap_or(self.created_at); + + let until_date = self.get_interest_until_date(product, now); + + let effective_term = if until_date > since_date { + until_date - since_date + } else { + return (0, 0); + }; + + self.get_interest_for_term(apy, effective_term) + } + + fn get_interest_for_term(&self, apy: UDecimal, term: Timestamp) -> (TokenAmount, u64) { + let term_in_milliseconds: u128 = term.into(); + + let yearly_interest = apy * self.principal; + + let ms_in_year: u128 = MS_IN_YEAR.into(); + + let interest = term_in_milliseconds * yearly_interest; + + // This will never fail because `MS_IN_YEAR` is u64 + // and remainder from u64 cannot be bigger than u64 so it is safe to unwrap here. + let remainder: u64 = (interest % ms_in_year).try_into().unwrap(); + let interest = interest / ms_in_year; + + (interest, remainder) + } + + fn get_interest_until_date(&self, product: &Product, now: Timestamp) -> Timestamp { + match product.terms.clone() { + Terms::Fixed(value) => cmp::min(now, self.created_at + value.lockup_term), + Terms::Flexible => now, + } + } + + fn is_liquidable(&self, now: Timestamp, term: Duration) -> bool { + now - self.created_at > term + } +} diff --git a/contract/src/product/model/mod.rs b/contract/src/product/model/mod.rs new file mode 100644 index 00000000..4bafae0a --- /dev/null +++ b/contract/src/product/model/mod.rs @@ -0,0 +1,2 @@ +pub(crate) mod v1; +pub(crate) mod v2; diff --git a/contract/src/product/model.rs b/contract/src/product/model/v1.rs similarity index 100% rename from contract/src/product/model.rs rename to contract/src/product/model/v1.rs diff --git a/contract/src/product/model/v2.rs b/contract/src/product/model/v2.rs new file mode 100644 index 00000000..e96dc5c6 --- /dev/null +++ b/contract/src/product/model/v2.rs @@ -0,0 +1,274 @@ +use std::{cmp, iter::Sum}; + +use near_sdk::{near, require}; +use sweat_jar_model::{ProductId, Score, ToAPY, TokenAmount, UDecimal, MS_IN_YEAR}; + +use crate::{ + common::{Duration, Timestamp}, + env, + jar::{ + account::{v2::AccountV2, versioned::Account}, + model::{Deposit, JarV2}, + }, + score::AccountScore, +}; + +/// The `Product` struct describes the terms of a deposit jar. It can be of Flexible or Fixed type. +#[near(serializers=[borsh, json])] +#[derive(Clone, Debug)] +pub struct ProductV2 { + /// The unique identifier of the product. + pub id: ProductId, + + /// The capacity boundaries of the deposit jar, specifying the minimum and maximum principal amount. + pub cap: Cap, + + /// The terms specific to the product, which can be either Flexible or Fixed. + pub terms: Terms, + + /// Describes whether a withdrawal fee is applicable and, if so, its details. + pub withdrawal_fee: Option, + + /// An optional ed25519 public key used for authorization to create a jar for this product. + pub public_key: Option>, + + /// Indicates whether it's possible to create a new jar for this product. + pub is_enabled: bool, +} + +/// The `Terms` enum describes additional terms specific to either Flexible or Fixed products. +#[near(serializers=[borsh, json])] +#[derive(Clone, Debug, PartialEq)] +#[serde(tag = "type", content = "data", rename_all = "snake_case")] +pub enum Terms { + /// Describes additional terms for Fixed products. + Fixed(FixedProductTerms), + + /// Describes additional terms for Flexible products. + Flexible(FlexibleProductTerms), + + /// TODO: doc + ScoreBased(ScoreBasedProductTerms), +} + +/// The `FixedProductTerms` struct contains terms specific to Fixed products. +#[near(serializers=[borsh, json])] +#[derive(Clone, Debug, PartialEq)] +pub struct FixedProductTerms { + /// The maturity term of the jar, during which it yields interest. After this period, the user can withdraw principal + /// or potentially restake the jar. + pub lockup_term: Duration, + pub apy: Apy, +} + +/// TODO: doc +#[near(serializers=[borsh, json])] +#[derive(Clone, Debug, PartialEq)] +pub struct FlexibleProductTerms { + pub apy: Apy, +} + +/// TODO: doc +#[near(serializers=[borsh, json])] +#[derive(Clone, Debug, PartialEq)] +pub struct ScoreBasedProductTerms { + pub score_cap: Score, + pub base_apy: Apy, + pub lockup_term: Duration, +} + +/// The `WithdrawalFee` enum describes withdrawal fee details, which can be either a fixed amount or a percentage of the withdrawal. +#[near(serializers=[borsh, json])] +#[derive(Clone, Debug, PartialEq)] +#[serde(tag = "type", content = "data", rename_all = "snake_case")] +pub enum WithdrawalFee { + /// Describes a fixed amount of tokens that a user must pay as a fee on withdrawal. + Fix(TokenAmount), + + /// Describes a percentage of the withdrawal amount that a user must pay as a fee on withdrawal. + Percent(UDecimal), +} + +/// The `Apy` enum describes the Annual Percentage Yield (APY) of the product, which can be either constant or downgradable. +#[near(serializers=[borsh, json])] +#[derive(Clone, Debug, PartialEq)] +#[serde(tag = "type", content = "data", rename_all = "snake_case")] +pub enum Apy { + /// Describes a constant APY, where the interest remains the same throughout the product's term. + Constant(UDecimal), + + /// Describes a downgradable APY, where an oracle can set a penalty if a user violates the product's terms. + Downgradable(DowngradableApy), +} + +/// The `DowngradableApy` struct describes an APY that can be downgraded by an oracle. +#[near(serializers=[borsh, json])] +#[derive(Clone, Debug, PartialEq)] +pub struct DowngradableApy { + /// The default APY value if the user meets all the terms of the product. + pub default: UDecimal, + + /// The fallback APY value if the user violates some of the terms of the product. + pub fallback: UDecimal, +} + +/// The `Cap` struct defines the capacity of a deposit jar in terms of the minimum and maximum allowed principal amounts. +#[near(serializers=[borsh, json])] +#[derive(Clone, Debug)] +pub struct Cap { + /// The minimum amount of tokens that can be stored in the jar. + pub min: TokenAmount, + + /// The maximum amount of tokens that can be stored in the jar. + pub max: TokenAmount, +} + +impl ProductV2 { + pub(crate) fn assert_cap(&self, amount: TokenAmount) { + if self.cap.min > amount || amount > self.cap.max { + env::panic_str(&format!( + "Total amount is out of product bounds: [{}..{}]", + self.cap.min, self.cap.max + )); + } + } + + pub(crate) fn assert_enabled(&self) { + require!(self.is_enabled, "It's not possible to create new jars for this product"); + } + + /// Check if fee in new product is not to high + pub(crate) fn assert_fee_amount(&self) { + let Some(ref fee) = self.withdrawal_fee else { + return; + }; + + let fee_ok = match fee { + WithdrawalFee::Fix(amount) => amount < &self.cap.min, + WithdrawalFee::Percent(percent) => percent.to_f32() < 100.0, + }; + + require!( + fee_ok, + "Fee for this product is too high. It is possible for customer to pay more in fees than he staked." + ); + } +} + +pub(crate) trait InterestCalculator { + fn get_interest(&self, account: AccountV2, jar: JarV2) -> (TokenAmount, UDecimal) { + let now = env::block_timestamp_ms(); + let since_date = jar.cache.map(|cache| cache.updated_at); + let apy = self.get_apy(&account); + + let (interest, remainder): (TokenAmount, u64) = jar + .deposits + .iter() + .map(|deposit| { + let term = self.get_interest_calculation_term(since_date, deposit); + + if term > 0 { + get_interest(deposit.principal, apy, term) + } else { + (0, 0) + } + }) + .fold((0, 0), |acc, (interest, remainder)| { + (acc.0 + interest, acc.1 + remainder) + }); + + let remainder: u64 = (remainder % MS_IN_YEAR.into()).try_into().unwrap(); + let extra_interest = remainder / MS_IN_YEAR.into(); + + (interest + extra_interest, remainder) + } + + fn get_apy(&self, account: &AccountV2) -> UDecimal; + + fn get_interest_calculation_term(&self, last_cached_at: Option, deposit: &Deposit) -> Duration; +} + +impl InterestCalculator for FixedProductTerms { + fn get_apy(&self, account: &AccountV2) -> UDecimal { + self.apy.get_effective(account.is_penalty_applied) + } + + fn get_interest_calculation_term(&self, last_cached_at: Option, deposit: &Deposit) -> Duration { + let since_date = last_cached_at.map_or(deposit.created_at, |cache_date| { + cmp::max(cache_date, deposit.created_at) + }); + let until_date = cmp::min(env::block_timestamp_ms(), deposit.created_at + self.lockup_term); + + until_date.checked_sub(since_date).unwrap_or(0) + } +} + +impl InterestCalculator for FlexibleProductTerms { + fn get_apy(&self, account: &AccountV2) -> UDecimal { + self.apy.get_effective(account.is_penalty_applied) + } + + fn get_interest_calculation_term(&self, last_cached_at: Option, deposit: &Deposit) -> Duration { + let since_date = last_cached_at.map_or(deposit.created_at, |cache_date| { + cmp::max(cache_date, deposit.created_at) + }); + + env::block_timestamp_ms() - since_date + } +} + +impl InterestCalculator for ScoreBasedProductTerms { + fn get_apy(&self, account: &AccountV2) -> UDecimal { + let score = account.score.claimable_score(); + + self.get_apy(&score, account.is_penalty_applied) + } + + fn get_interest_calculation_term(&self, last_cached_at: Option, deposit: &Deposit) -> Timestamp { + let since_date = last_cached_at.map_or(deposit.created_at, |cache_date| { + cmp::max(cache_date, deposit.created_at) + }); + let until_date = cmp::min(env::block_timestamp_ms(), deposit.created_at + self.lockup_term); + + until_date.checked_sub(since_date).unwrap_or(0) + } +} + +impl ScoreBasedProductTerms { + fn get_apy(&self, score: &[Score], is_penalty_applied: bool) -> UDecimal { + let total_score: Score = score.iter().map(|score| score.min(&self.score_cap)).sum(); + self.base_apy.get_effective(is_penalty_applied) + total_score.to_apy() + } +} + +fn get_interest(principal: TokenAmount, apy: UDecimal, term: Duration) -> (TokenAmount, u64) { + let term_in_milliseconds: u128 = term.into(); + + let yearly_interest = apy * principal; + + let ms_in_year: u128 = MS_IN_YEAR.into(); + + let interest = term_in_milliseconds * yearly_interest; + + // This will never fail because `MS_IN_YEAR` is u64 + // and remainder from u64 cannot be bigger than u64 so it is safe to unwrap here. + let remainder: u64 = (interest % ms_in_year).try_into().unwrap(); + let interest = interest / ms_in_year; + + (interest, remainder) +} + +impl Apy { + fn get_effective(&self, is_penalty_applied: bool) -> UDecimal { + match self { + Apy::Constant(apy) => apy.clone(), + Apy::Downgradable(apy) => { + if is_penalty_applied { + apy.fallback + } else { + apy.default + } + } + } + } +} From 26043c7b33fa188b22c2498ada4e8d26c1042d43 Mon Sep 17 00:00:00 2001 From: Vasily Styagov Date: Wed, 9 Oct 2024 18:11:58 +0100 Subject: [PATCH 02/93] refactor claim --- contract/src/claim/api.rs | 175 ++++------ contract/src/internal.rs | 4 +- contract/src/jar/account/v2.rs | 93 +++++- contract/src/jar/api.rs | 8 + contract/src/jar/mod.rs | 1 + contract/src/jar/model/common.rs | 388 ++++------------------ contract/src/jar/model/v2.rs | 100 +++--- contract/src/jar/verification/mod.rs | 1 + contract/src/jar/verification/model.rs | 94 ++++++ contract/src/lib.rs | 9 +- contract/src/migration/aggregated_jars.rs | 0 contract/src/migration/mod.rs | 1 + contract/src/product/api.rs | 17 +- contract/src/product/model/v2.rs | 34 +- contract/src/score/account_score.rs | 1 + model/src/claimed_amount_view.rs | 6 +- model/src/jar.rs | 2 +- 17 files changed, 435 insertions(+), 499 deletions(-) create mode 100644 contract/src/jar/verification/mod.rs create mode 100644 contract/src/jar/verification/model.rs create mode 100644 contract/src/migration/aggregated_jars.rs diff --git a/contract/src/claim/api.rs b/contract/src/claim/api.rs index db314ba1..34602b50 100644 --- a/contract/src/claim/api.rs +++ b/contract/src/claim/api.rs @@ -1,14 +1,17 @@ +use std::collections::HashMap; + use near_sdk::{env, ext_contract, json_types::U128, near_bindgen, AccountId, PromiseOrValue}; -use sweat_jar_model::{ - api::ClaimApi, claimed_amount_view::ClaimedAmountView, jar::AggregatedTokenAmountView, TokenAmount, JAR_BATCH_SIZE, -}; +use sweat_jar_model::{api::ClaimApi, claimed_amount_view::ClaimedAmountView, jar::AggregatedTokenAmountView}; use crate::{ common::Timestamp, - event::{emit, ClaimEventItem, EventKind}, + event::{emit, EventKind}, internal::is_promise_success, - jar::model::Jar, - score::AccountScore, + jar::{ + account::v2::AccountV2Companion, + model::{JarV2, JarV2Companion}, + }, + product::model::v2::InterestCalculator, Contract, ContractExt, JarsStorage, }; @@ -17,11 +20,10 @@ use crate::{ pub trait ClaimCallbacks { fn after_claim( &mut self, + account_id: AccountId, claimed_amount: ClaimedAmountView, - jars_before_transfer: Vec, - score_before_transfer: Option, + account_rollback: AccountV2Companion, event: EventKind, - now: Timestamp, ) -> ClaimedAmountView; } @@ -40,53 +42,43 @@ impl Contract { account_id: AccountId, detailed: Option, ) -> PromiseOrValue { - let now = env::block_timestamp_ms(); + let account = self.accounts_v2.get_mut(&account_id).expect("Account is not found"); let mut accumulator = ClaimedAmountView::new(detailed); + let now = env::block_timestamp_ms(); - let account_jars = self.account_jars(&account_id); - - let account_score = self.get_score_mut(&account_id); - - let account_score_before_transfer = account_score.as_ref().map(|s| **s); - - let score = account_score.map(AccountScore::claim_score).unwrap_or_default(); - - let mut unlocked_jars: Vec<((TokenAmount, u64), &Jar)> = account_jars - .iter() - .filter(|jar| !jar.is_pending_withdraw) - .map(|jar| { - let product = self.get_product(&jar.product_id); - (jar.get_interest(&score, &product, now), jar) - }) - .collect(); - - unlocked_jars.sort_by(|a, b| b.0 .0.cmp(&a.0 .0)); + let mut rollback_jars = HashMap::new(); + for (product_id, jar) in account.jars.iter_mut() { + if jar.is_pending_withdraw { + continue; + } - let jars_to_claim: Vec<_> = unlocked_jars.into_iter().take(JAR_BATCH_SIZE).collect(); + rollback_jars.insert(product_id, jar.to_rollback()); - let mut event_data: Vec = vec![]; + let product = self.products.get(product_id).expect("Product is not found"); + let (interest, remainder) = product.terms.get_interest(account, jar); - for ((available_interest, remainder), jar) in &jars_to_claim { - if *available_interest > 0 { - let jar = self.get_jar_mut_internal(&jar.account_id, jar.id); + if interest == 0 { + continue; + } - jar.claim_remainder = *remainder; + jar.claim(interest, remainder, now).lock(); - jar.claim(*available_interest, now).lock(); + accumulator.add(product_id, interest); + } - accumulator.add(jar.id, *available_interest); + let mut account_rollback = AccountV2Companion::default(); + account_rollback.score = Some(account.score); + account_rollback.jars = Some(*rollback_jars); - event_data.push((jar.id, U128(*available_interest))); - } - } + account.score.claim_score(); if accumulator.get_total().0 > 0 { self.claim_interest( &account_id, accumulator, - jars_to_claim.into_iter().map(|a| a.1).cloned().collect(), - account_score_before_transfer, - EventKind::Claim(event_data), + account_rollback, + // TODO: add events + EventKind::Claim(vec![]), now, ) } else { @@ -99,19 +91,17 @@ impl Contract { #[cfg(test)] fn claim_interest( &mut self, - _account_id: &AccountId, + account_id: &AccountId, claimed_amount: ClaimedAmountView, - jars_before_transfer: Vec, - score_before_transfer: Option, + account_rollback: AccountV2Companion, event: EventKind, now: Timestamp, ) -> PromiseOrValue { PromiseOrValue::Value(self.after_claim_internal( + account_id.clone(), claimed_amount, - jars_before_transfer, - score_before_transfer, + account_rollback, event, - now, is_promise_success(), )) } @@ -122,10 +112,8 @@ impl Contract { &mut self, account_id: &AccountId, claimed_amount: ClaimedAmountView, - jars_before_transfer: Vec, - score_before_transfer: Option, + account_rollback: AccountV2Companion, event: EventKind, - now: Timestamp, ) -> PromiseOrValue { use crate::{ common::gas_data::{GAS_FOR_AFTER_CLAIM, GAS_FOR_FT_TRANSFER}, @@ -134,77 +122,45 @@ impl Contract { }; assert_gas(GAS_FOR_FT_TRANSFER.as_gas() * 2 + GAS_FOR_AFTER_CLAIM.as_gas(), || { - format!("claim_interest: number of jars: {}", jars_before_transfer.len()) + "Not enough gas for claim".to_string() }); self.ft_contract() .ft_transfer(account_id, claimed_amount.get_total().0, "claim", &None) .then(after_claim_call( + account_id.clone(), claimed_amount, - jars_before_transfer, - score_before_transfer, + account_rollback, event, - now, )) .into() } fn after_claim_internal( &mut self, + account_id: AccountId, claimed_amount: ClaimedAmountView, - jars_before_transfer: Vec, - score_before_transfer: Option, + account_rollback: AccountV2Companion, event: EventKind, - now: Timestamp, is_promise_success: bool, ) -> ClaimedAmountView { if is_promise_success { - for jar_before_transfer in jars_before_transfer { - let product = self.products.get(&jar_before_transfer.product_id).unwrap_or_else(|| { - env::panic_str(&format!("Product '{}' doesn't exist", jar_before_transfer.product_id)) - }); - - let score = self - .get_score(&jar_before_transfer.account_id) - .map(AccountScore::claimable_score) - .unwrap_or_default(); - - let jar = self - .accounts - .get_mut(&jar_before_transfer.account_id) - .unwrap_or_else(|| { - env::panic_str(&format!("Account '{}' doesn't exist", jar_before_transfer.account_id)) - }) - .get_jar_mut(jar_before_transfer.id); + let account = self.accounts_v2.get_mut(&account_id).expect("Account is not found"); + let jars = account_rollback.jars.expect("Jars are required in rollback account"); + for (product_id, _) in jars { + let jar = account.jars.get_mut(&product_id).expect("Jar is not found"); jar.unlock(); - if jar.should_be_closed(&score, &product, now) { - self.delete_jar(&jar_before_transfer.account_id, jar_before_transfer.id); - } + // TODO: check if should delete jar } emit(event); claimed_amount } else { - let account_id = jars_before_transfer - .first() - .expect("After claim without jars") - .account_id - .clone(); - - for jar_before_transfer in jars_before_transfer { - let jar_id = jar_before_transfer.id; - *self.get_jar_mut_internal(&account_id, jar_id) = jar_before_transfer.unlocked(); - } - - if let Some(score) = score_before_transfer { - self.accounts - .get_mut(&account_id) - .unwrap_or_else(|| panic!("Account: {account_id} does not exist")) - .score = score; - } + let account = self.accounts_v2.get_mut(&account_id).expect("Account is not found"); + account.apply(&account_rollback); match claimed_amount { ClaimedAmountView::Total(_) => ClaimedAmountView::Total(U128(0)), @@ -219,18 +175,16 @@ impl ClaimCallbacks for Contract { #[private] fn after_claim( &mut self, + account_id: AccountId, claimed_amount: ClaimedAmountView, - jars_before_transfer: Vec, - score_before_transfer: Option, + account_rollback: AccountV2Companion, event: EventKind, - now: Timestamp, ) -> ClaimedAmountView { self.after_claim_internal( + account_id, claimed_amount, - jars_before_transfer, - score_before_transfer, + account_rollback, event, - now, is_promise_success(), ) } @@ -239,13 +193,24 @@ impl ClaimCallbacks for Contract { #[cfg(not(test))] #[mutants::skip] // Covered by integration tests fn after_claim_call( + account_id: AccountId, claimed_amount: ClaimedAmountView, - jars_before_transfer: Vec, - score_before_transfer: Option, + account_rollback: AccountV2Companion, event: EventKind, - now: Timestamp, ) -> near_sdk::Promise { ext_self::ext(env::current_account_id()) .with_static_gas(crate::common::gas_data::GAS_FOR_AFTER_CLAIM) - .after_claim(claimed_amount, jars_before_transfer, score_before_transfer, event, now) + .after_claim(account_id, claimed_amount, account_rollback, event) +} + +impl JarV2 { + fn to_rollback(&self) -> JarV2Companion { + let mut companion = JarV2Companion::default(); + companion.is_pending_withdraw = Some(false); + companion.claimed_balance = Some(self.claimed_balance); + companion.claim_remainder = Some(self.claim_remainder); + companion.cache = Some(self.cache); + + companion + } } diff --git a/contract/src/internal.rs b/contract/src/internal.rs index 645b3ad8..45549867 100644 --- a/contract/src/internal.rs +++ b/contract/src/internal.rs @@ -6,7 +6,7 @@ use sweat_jar_model::{ ProductId, }; -use crate::{env, jar::model::Jar, AccountId, Contract, Product}; +use crate::{env, jar::model::Jar, product::model::v2::ProductV2, AccountId, Contract, Product}; impl Contract { pub(crate) fn assert_manager(&self) { @@ -73,7 +73,7 @@ impl Contract { // UnorderedMap doesn't have cache and deserializes `Product` on each get // This cached getter significantly reduces gas usage - pub fn get_product(&self, product_id: &ProductId) -> Product { + pub fn get_product(&self, product_id: &ProductId) -> ProductV2 { self.products_cache .borrow_mut() .entry(product_id.clone()) diff --git a/contract/src/jar/account/v2.rs b/contract/src/jar/account/v2.rs index a3b47b90..8c8ed813 100644 --- a/contract/src/jar/account/v2.rs +++ b/contract/src/jar/account/v2.rs @@ -1,12 +1,17 @@ -use std::ops::{Deref, DerefMut}; +use std::{ + collections::HashMap, + ops::{Deref, DerefMut}, +}; -use near_sdk::near; -use sweat_jar_model::jar::JarId; +use near_sdk::{env, env::panic_str, near, AccountId}; +use sweat_jar_model::{jar::JarId, ProductId, Timezone, TokenAmount}; use crate::{ - jar::model::{AccountJarsLegacy, Jar}, + common::Timestamp, + jar::model::{AccountJarsLegacy, Deposit, Jar, JarV2, JarV2Companion}, migration::account_jars_non_versioned::AccountJarsNonVersioned, score::AccountScore, + Contract, }; #[near] @@ -14,7 +19,85 @@ use crate::{ pub struct AccountV2 { /// Is used as nonce in `get_ticket_hash` method. pub nonce: u32, - pub jars: Vec, + pub jars: HashMap, pub score: AccountScore, pub is_penalty_applied: bool, } + +#[near(serializers=[json])] +#[derive(Default, Debug, PartialEq)] +pub struct AccountV2Companion { + pub nonce: Option, + pub jars: Option>, + pub score: Option, + pub is_penalty_applied: Option, +} + +impl Contract { + pub(crate) fn get_or_create_account_mut(&mut self, account_id: &AccountId) -> &mut AccountV2 { + if !self.accounts_v2.contains_key(&account_id) { + self.accounts_v2.insert(account_id.clone(), AccountV2::default()); + } + + self.accounts_v2.get_mut(account_id).expect("Account is not presented") + } +} + +impl AccountV2 { + pub(crate) fn deposit(&mut self, product_id: &ProductId, principal: TokenAmount) { + let deposit = Deposit::new(env::block_timestamp_ms(), principal); + + if let Some(jar) = self.jars.get_mut(product_id) { + jar.deposits.push(deposit); + } else { + let mut jar = JarV2::default(); + jar.deposits.push(deposit); + + self.jars.insert(product_id.clone(), jar); + } + } + + pub(crate) fn clean_up_jars(&mut self, product_id: &ProductId) { + if let Some(jar) = self.jars.get_mut(product_id) { + if let Some(last_index_to_remove) = jar.deposits.iter().position(|deposit| deposit.principal != 0) { + jar.deposits.drain(0..=last_index_to_remove); + } + } + } + + pub(crate) fn try_set_timezone(&mut self, timezone: Option) { + match (timezone, self.score.borrow_mut()) { + // Time zone already set. No actions required. + (Some(_) | None, Some(_)) => (), + (Some(timezone), None) => { + self.score = AccountScore::new(timezone); + } + (None, None) => { + panic_str(&format!( + "Trying to create step base jar for account: '{account_id}' without providing time zone" + )); + } + } + } + + pub(crate) fn apply(&mut self, companion: &AccountV2Companion) { + if let Some(nonce) = companion.nonce { + self.nonce = nonce; + } + + if let Some(jars) = companion.jars.iter() { + for (product_id, jar_companion) in jars { + let jar = self.jars.get_mut(&product_id).expect("Jar is not found"); + jar.apply(jar_companion); + } + } + + if let Some(score) = companion.score { + self.score = score; + } + + if let Some(is_penalty_applied) = companion.is_penalty_applied { + self.is_penalty_applied = is_penalty_applied; + } + } +} diff --git a/contract/src/jar/api.rs b/contract/src/jar/api.rs index 50db2149..708db296 100644 --- a/contract/src/jar/api.rs +++ b/contract/src/jar/api.rs @@ -71,6 +71,7 @@ impl Contract { #[near_bindgen] impl JarApi for Contract { // TODO: restore previous version after V2 migration + // TODO: add v2 support #[mutants::skip] fn get_jar(&self, account_id: AccountId, jar_id: JarIdView) -> JarView { if let Some(record) = self.account_jars_v1.get(&account_id) { @@ -103,10 +104,12 @@ impl JarApi for Contract { .into() } + // TODO: add v2 support fn get_jars_for_account(&self, account_id: AccountId) -> Vec { self.account_jars(&account_id).iter().map(Into::into).collect() } + // TODO: add v2 support fn get_total_principal(&self, account_id: AccountId) -> AggregatedTokenAmountView { self.get_principal( self.account_jars(&account_id).iter().map(|a| U32(a.id)).collect(), @@ -114,6 +117,7 @@ impl JarApi for Contract { ) } + // TODO: add v2 support fn get_principal(&self, jar_ids: Vec, account_id: AccountId) -> AggregatedTokenAmountView { let mut detailed_amounts = HashMap::::new(); let mut total_amount: TokenAmount = 0; @@ -132,6 +136,7 @@ impl JarApi for Contract { } } + // TODO: add v2 support fn get_total_interest(&self, account_id: AccountId) -> AggregatedInterestView { self.get_interest( self.account_jars(&account_id).iter().map(|a| U32(a.id)).collect(), @@ -139,6 +144,7 @@ impl JarApi for Contract { ) } + // TODO: add v2 support fn get_interest(&self, jar_ids: Vec, account_id: AccountId) -> AggregatedInterestView { let now = env::block_timestamp_ms(); @@ -168,6 +174,7 @@ impl JarApi for Contract { } } + // TODO: add v2 support fn restake(&mut self, jar_id: JarIdView) -> JarView { self.migrate_account_if_needed(&env::predecessor_account_id()); let (old_id, jar) = self.restake_internal(jar_id); @@ -218,6 +225,7 @@ impl JarApi for Contract { result } + // TODO: add v2 support fn unlock_jars_for_account(&mut self, account_id: AccountId) { self.assert_manager(); self.migrate_account_if_needed(&account_id); diff --git a/contract/src/jar/mod.rs b/contract/src/jar/mod.rs index 0bcf03bd..d4358e6e 100644 --- a/contract/src/jar/mod.rs +++ b/contract/src/jar/mod.rs @@ -2,4 +2,5 @@ pub mod account; pub mod api; pub mod model; mod tests; +mod verification; pub mod view; diff --git a/contract/src/jar/model/common.rs b/contract/src/jar/model/common.rs index 82b1f453..301d2d61 100644 --- a/contract/src/jar/model/common.rs +++ b/contract/src/jar/model/common.rs @@ -1,24 +1,12 @@ -use std::cmp; - -use ed25519_dalek::{Signature, VerifyingKey, PUBLIC_KEY_LENGTH, SIGNATURE_LENGTH}; use near_sdk::{ env, - env::{panic_str, sha256}, json_types::{Base64VecU8, U128, U64}, - near, require, AccountId, -}; -use sweat_jar_model::{ - jar::{JarId, JarView}, - ProductId, Score, Timezone, TokenAmount, UDecimal, MS_IN_DAY, MS_IN_YEAR, + near, AccountId, }; +use sweat_jar_model::{jar::JarId, Timezone, TokenAmount}; use crate::{ - common::Timestamp, - event::{emit, EventKind, TopUpData}, - jar::model::{Jar, JarLastVersion}, - product::model::{Apy, Product, Terms}, - score::AccountScore, - Contract, JarsStorage, + common::Timestamp, jar::model::Jar, product::model::v2::Terms, score::AccountScore, Contract, JarsStorage, }; /// The `JarTicket` struct represents a request to create a deposit jar for a corresponding product. @@ -45,159 +33,6 @@ pub struct JarTicket { pub timezone: Option, } -impl JarLastVersion { - pub(crate) fn lock(&mut self) { - self.is_pending_withdraw = true; - } - - pub(crate) fn unlock(&mut self) { - self.is_pending_withdraw = false; - } - - pub(crate) fn apply_penalty(&mut self, product: &Product, is_applied: bool, now: Timestamp) { - assert!( - !product.is_score_product(), - "Applying penalty is not supported for score based jars" - ); - - let (interest, remainder) = self.get_interest(&[], product, now); - - self.claim_remainder = remainder; - - self.cache = Some(JarCache { - updated_at: now, - interest, - }); - self.is_penalty_applied = is_applied; - } - - pub(crate) fn top_up(&mut self, amount: TokenAmount, product: &Product, now: Timestamp) -> &mut Self { - assert!( - !product.is_score_product(), - "Top up is not supported for score based jars" - ); - - let current_interest = self.get_interest(&[], product, now).0; - - self.principal += amount; - self.cache = Some(JarCache { - updated_at: now, - interest: current_interest, - }); - self - } - - pub(crate) fn claim(&mut self, claimed_amount: TokenAmount, now: Timestamp) -> &mut Self { - self.claimed_balance += claimed_amount; - - self.cache = Some(JarCache { - updated_at: now, - interest: 0, - }); - self - } - - pub(crate) fn should_be_closed(&self, score: &[Score], product: &Product, now: Timestamp) -> bool { - !product.is_flexible() && self.principal == 0 && self.get_interest(score, product, now).0 == 0 - } - - /// Indicates whether a user can withdraw tokens from the jar at the moment or not. - /// For a Flexible product withdrawal is always possible. - /// For Fixed product it's defined by the lockup term. - pub(crate) fn is_liquidable(&self, product: &Product, now: Timestamp) -> bool { - match &product.terms { - Terms::Fixed(value) => now - self.created_at > value.lockup_term, - Terms::Flexible => true, - } - } - - pub(crate) fn is_empty(&self) -> bool { - self.principal == 0 - } - - fn get_interest_for_term(&self, cache: u128, apy: UDecimal, term: Timestamp) -> (TokenAmount, u64) { - let term_in_milliseconds: u128 = term.into(); - - let yearly_interest = apy * self.principal; - - let ms_in_year: u128 = MS_IN_YEAR.into(); - - let interest = term_in_milliseconds * yearly_interest; - - // This will never fail because `MS_IN_YEAR` is u64 - // and remainder from u64 cannot be bigger than u64 so it is safe to unwrap here. - let remainder: u64 = (interest % ms_in_year).try_into().unwrap(); - - let interest = interest / ms_in_year; - - let total_remainder = self.claim_remainder + remainder; - - ( - cache + interest + u128::from(total_remainder / MS_IN_YEAR), - total_remainder % MS_IN_YEAR, - ) - } - - fn get_interest_with_apy(&self, apy: UDecimal, product: &Product, now: Timestamp) -> (TokenAmount, u64) { - let (base_date, cache_interest) = if let Some(cache) = &self.cache { - (cache.updated_at, cache.interest) - } else { - (self.created_at, 0) - }; - - let until_date = self.get_interest_until_date(product, now); - - let effective_term = if until_date > base_date { - until_date - base_date - } else { - return (cache_interest, 0); - }; - - self.get_interest_for_term(cache_interest, apy, effective_term) - } - - fn get_score_interest(&self, score: &[Score], product: &Product, now: Timestamp) -> (TokenAmount, u64) { - let cache = self.cache.map(|c| c.interest).unwrap_or_default(); - - if let Terms::Fixed(end_term) = &product.terms { - if now > end_term.lockup_term { - return (cache, 0); - } - } - - let apy = product.apy_for_score(score); - self.get_interest_for_term(cache, apy, MS_IN_DAY) - } - - pub(crate) fn get_interest(&self, score: &[Score], product: &Product, now: Timestamp) -> (TokenAmount, u64) { - if product.is_score_product() { - self.get_score_interest(score, product, now) - } else { - self.get_interest_with_apy(self.get_apy(product), product, now) - } - } - - pub(crate) fn get_apy(&self, product: &Product) -> UDecimal { - match product.apy.clone() { - Apy::Constant(apy) => apy, - Apy::Downgradable(apy) => { - if self.is_penalty_applied { - apy.fallback - } else { - apy.default - } - } - } - } - - fn get_interest_until_date(&self, product: &Product, now: Timestamp) -> Timestamp { - match product.terms.clone() { - Terms::Fixed(value) => cmp::min(now, self.created_at + value.lockup_term), - Terms::Flexible => now, - } - } -} - /// A cached value that stores calculated interest based on the current state of the jar. /// This cache is updated whenever properties that impact interest calculation change, /// allowing for efficient interest calculations between state changes. @@ -209,86 +44,91 @@ pub struct JarCache { } impl Contract { - pub(crate) fn create_jar( + pub(crate) fn deposit( &mut self, account_id: AccountId, ticket: JarTicket, amount: U128, signature: Option, - ) -> JarView { + ) { let amount = amount.0; let product_id = &ticket.product_id; let product = self.get_product(product_id); - if product.is_score_product() { - match (ticket.timezone, self.get_score_mut(&account_id)) { - // Time zone already set. No actions required. - (Some(_) | None, Some(_)) => (), - (Some(timezone), None) => { - self.accounts.entry(account_id.clone()).or_default().score = AccountScore::new(timezone); - } - (None, None) => { - panic_str(&format!( - "Trying to create step base jar for account: '{account_id}' without providing time zone" - )); - } - } - } - product.assert_enabled(); product.assert_cap(amount); self.verify(&account_id, amount, &ticket, signature); - let id = self.increment_and_get_last_jar_id(); - let now = env::block_timestamp_ms(); - let jar = Jar::create(id, account_id.clone(), product_id.clone(), amount, now); - - self.add_new_jar(&account_id, jar.clone()); - - emit(EventKind::CreateJar(jar.clone().into())); - - jar.into() - } - - pub(crate) fn top_up(&mut self, account: &AccountId, jar_id: JarId, amount: U128) -> U128 { - self.migrate_account_if_needed(account); - - let jar = self.get_jar_internal(account, jar_id).clone(); - let product = self.get_product(&jar.product_id).clone(); - - require!(product.allows_top_up(), "The product doesn't allow top-ups"); - product.assert_cap(jar.principal + amount.0); - - let now = env::block_timestamp_ms(); - - let principal = self - .get_jar_mut_internal(account, jar_id) - .top_up(amount.0, &product, now) - .principal; - - emit(EventKind::TopUp(TopUpData { id: jar_id, amount })); - - U128(principal) - } - - pub(crate) fn delete_jar(&mut self, account_id: &AccountId, jar_id: JarId) { - let jars = self - .accounts - .get_mut(account_id) - .unwrap_or_else(|| panic_str(&format!("Account '{account_id}' doesn't exist"))); + let account = self.get_or_create_account_mut(&account_id); - require!( - !jars.is_empty(), - "Trying to delete a jar from account without any jars." - ); - - let jar_position = jars - .iter() - .position(|j| j.id == jar_id) - .unwrap_or_else(|| panic_str(&format!("Jar with id {jar_id} doesn't exist"))); + if matches!(product.terms, Terms::ScoreBased(_)) { + account.try_set_timezone(ticket.timezone); + } - jars.swap_remove(jar_position); - } + let account = self.get_or_create_account_mut(&account_id); + account.deposit(product_id, amount); + } + + // pub(crate) fn create_jar( + // &mut self, + // account_id: AccountId, + // ticket: JarTicket, + // amount: U128, + // signature: Option, + // ) -> JarView { + // let amount = amount.0; + // let product_id = &ticket.product_id; + // let product = self.get_product(product_id); + // + // product.assert_enabled(); + // product.assert_cap(amount); + // + // if matches!(product.terms, Terms::ScoreBased(_)) { + // match (ticket.timezone, self.get_score_mut(&account_id)) { + // // Time zone already set. No actions required. + // (Some(_) | None, Some(_)) => (), + // (Some(timezone), None) => { + // self.accounts.entry(account_id.clone()).or_default().score = AccountScore::new(timezone); + // } + // (None, None) => { + // panic_str(&format!( + // "Trying to create step base jar for account: '{account_id}' without providing time zone" + // )); + // } + // } + // } + // + // self.verify(&account_id, amount, &ticket, signature); + // + // let id = self.increment_and_get_last_jar_id(); + // let now = env::block_timestamp_ms(); + // let jar = Jar::create(id, account_id.clone(), product_id.clone(), amount, now); + // + // self.add_new_jar(&account_id, jar.clone()); + // + // emit(EventKind::CreateJar(jar.clone().into())); + // + // jar.into() + // } + + // pub(crate) fn delete_jar(&mut self, account_id: &AccountId, jar_id: JarId) { + // let jars = self + // .accounts + // .get_mut(account_id) + // .unwrap_or_else(|| panic_str(&format!("Account '{account_id}' doesn't exist"))); + // + // require!( + // !jars.is_empty(), + // "Trying to delete a jar from account without any jars." + // ); + // + // let jar_position = jars + // .iter() + // .position(|j| j.id == jar_id) + // .unwrap_or_else(|| panic_str(&format!("Jar with id {jar_id} doesn't exist"))); + // + // jars.swap_remove(jar_position); + // } pub(crate) fn get_score(&self, account: &AccountId) -> Option<&AccountScore> { self.accounts.get(account).and_then(|a| a.score()) @@ -332,86 +172,4 @@ impl Contract { .get_jar(id) .clone() } - - pub(crate) fn verify( - &mut self, - account_id: &AccountId, - amount: TokenAmount, - ticket: &JarTicket, - signature: Option, - ) { - self.migrate_account_if_needed(account_id); - - let last_jar_id = self.accounts.get(account_id).map(|jars| jars.last_id); - let product = self.get_product(&ticket.product_id); - - if let Some(pk) = &product.public_key { - let Some(signature) = signature else { - panic_str("Signature is required"); - }; - - let is_time_valid = env::block_timestamp_ms() <= ticket.valid_until.0; - require!(is_time_valid, "Ticket is outdated"); - - let hash = Self::get_ticket_hash(account_id, amount, ticket, last_jar_id); - let is_signature_valid = Self::verify_signature(&signature.0, pk, &hash); - - require!(is_signature_valid, "Not matching signature"); - } - } - - fn get_ticket_hash( - account_id: &AccountId, - amount: TokenAmount, - ticket: &JarTicket, - last_jar_id: Option, - ) -> Vec { - sha256( - Self::get_signature_material( - &env::current_account_id(), - account_id, - &ticket.product_id, - amount, - ticket.valid_until.0, - last_jar_id, - ) - .as_bytes(), - ) - } - - pub(crate) fn get_signature_material( - contract_account_id: &AccountId, - receiver_account_id: &AccountId, - product_id: &ProductId, - amount: TokenAmount, - valid_until: Timestamp, - last_jar_id: Option, - ) -> String { - format!( - "{},{},{},{},{},{}", - contract_account_id, - receiver_account_id, - product_id, - amount, - last_jar_id.map_or_else(String::new, |value| value.to_string()), - valid_until, - ) - } - - fn verify_signature(signature: &[u8], product_public_key: &[u8], ticket_hash: &[u8]) -> bool { - let signature_bytes: &[u8; SIGNATURE_LENGTH] = signature - .try_into() - .unwrap_or_else(|_| panic!("Signature must be {SIGNATURE_LENGTH} bytes")); - - let signature = Signature::from_bytes(signature_bytes); - - let public_key_bytes: &[u8; PUBLIC_KEY_LENGTH] = product_public_key - .try_into() - .unwrap_or_else(|_| panic!("Public key must be {PUBLIC_KEY_LENGTH} bytes")); - - VerifyingKey::from_bytes(public_key_bytes) - .expect("Public key is invalid") - .verify_strict(ticket_hash, &signature) - .is_ok() - } } diff --git a/contract/src/jar/model/v2.rs b/contract/src/jar/model/v2.rs index c28a30f3..f3dbc751 100644 --- a/contract/src/jar/model/v2.rs +++ b/contract/src/jar/model/v2.rs @@ -1,11 +1,14 @@ -use near_sdk::{near, AccountId}; -use sweat_jar_model::{jar::JarId, ProductId, TokenAmount, UDecimal, MS_IN_YEAR}; +use near_sdk::near; +use sweat_jar_model::{TokenAmount, UDecimal}; -use crate::{common::Timestamp, jar::model::JarCache}; +use crate::{ + common::{Duration, Timestamp}, + jar::model::JarCache, +}; /// The `Jar` struct represents a deposit jar within the smart contract. #[near(serializers=[borsh, json])] -#[derive(Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)] +#[derive(Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd, Default)] pub struct JarV2 { pub deposits: Vec, pub cache: Option, @@ -14,6 +17,16 @@ pub struct JarV2 { pub claim_remainder: u64, } +#[near(serializers=[json])] +#[derive(Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd, Default)] +pub struct JarV2Companion { + pub deposits: Option>, + pub cache: Option>, + pub claimed_balance: Option, + pub is_pending_withdraw: Option, + pub claim_remainder: Option, +} + #[near(serializers=[borsh, json])] #[derive(Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)] pub struct Deposit { @@ -21,52 +34,61 @@ pub struct Deposit { pub principal: TokenAmount, } -impl Deposit { - fn get_interest_with_apy( - &self, - apy: UDecimal, - product: &Product, - now: Timestamp, - since_date: Option, - ) -> (TokenAmount, u64) { - let since_date = since_date.unwrap_or(self.created_at); - - let until_date = self.get_interest_until_date(product, now); - - let effective_term = if until_date > since_date { - until_date - since_date - } else { - return (0, 0); - }; - - self.get_interest_for_term(apy, effective_term) +impl JarV2 { + pub(crate) fn lock(&mut self) -> &mut Self { + self.is_pending_withdraw = true; + + self } - fn get_interest_for_term(&self, apy: UDecimal, term: Timestamp) -> (TokenAmount, u64) { - let term_in_milliseconds: u128 = term.into(); + pub(crate) fn unlock(&mut self) -> &mut Self { + self.is_pending_withdraw = false; - let yearly_interest = apy * self.principal; + self + } - let ms_in_year: u128 = MS_IN_YEAR.into(); + pub(crate) fn claim(&mut self, claimed_amount: TokenAmount, remainder: u64, now: Timestamp) -> &mut Self { + self.claimed_balance += claimed_amount; + self.claim_remainder = remainder; + self.cache = Some(JarCache { + updated_at: now, + interest: 0, + }); - let interest = term_in_milliseconds * yearly_interest; + self + } - // This will never fail because `MS_IN_YEAR` is u64 - // and remainder from u64 cannot be bigger than u64 so it is safe to unwrap here. - let remainder: u64 = (interest % ms_in_year).try_into().unwrap(); - let interest = interest / ms_in_year; + pub(crate) fn apply(&mut self, companion: JarV2Companion) -> &mut Self { + if let Some(claim_remainder) = companion.claim_remainder { + self.claim_remainder = claim_remainder; + } - (interest, remainder) - } + if let Some(claimed_balance) = companion.claimed_balance { + self.claimed_balance = claimed_balance; + } - fn get_interest_until_date(&self, product: &Product, now: Timestamp) -> Timestamp { - match product.terms.clone() { - Terms::Fixed(value) => cmp::min(now, self.created_at + value.lockup_term), - Terms::Flexible => now, + if let Some(cache) = companion.cache { + self.cache = cache; } + + if let Some(deposits) = companion.deposits { + self.deposits = deposits; + } + + if let Some(is_pending_withdraw) = companion.is_pending_withdraw { + self.is_pending_withdraw = is_pending_withdraw; + } + + self + } +} + +impl Deposit { + pub(crate) fn new(created_at: Timestamp, principal: TokenAmount) -> Self { + Self { created_at, principal } } - fn is_liquidable(&self, now: Timestamp, term: Duration) -> bool { + pub(crate) fn is_liquidable(&self, now: Timestamp, term: Duration) -> bool { now - self.created_at > term } } diff --git a/contract/src/jar/verification/mod.rs b/contract/src/jar/verification/mod.rs new file mode 100644 index 00000000..ee2d47ae --- /dev/null +++ b/contract/src/jar/verification/mod.rs @@ -0,0 +1 @@ +mod model; diff --git a/contract/src/jar/verification/model.rs b/contract/src/jar/verification/model.rs new file mode 100644 index 00000000..54d56b59 --- /dev/null +++ b/contract/src/jar/verification/model.rs @@ -0,0 +1,94 @@ +use ed25519_dalek::{Signature, VerifyingKey, PUBLIC_KEY_LENGTH, SIGNATURE_LENGTH}; +use near_sdk::{ + env, + env::{panic_str, sha256}, + json_types::Base64VecU8, + require, AccountId, +}; +use sweat_jar_model::{jar::JarId, ProductId, TokenAmount}; + +use crate::{common::Timestamp, jar::model::JarTicket, Contract}; + +impl Contract { + pub(crate) fn verify( + &mut self, + account_id: &AccountId, + amount: TokenAmount, + ticket: &JarTicket, + signature: Option, + ) { + self.migrate_account_if_needed(account_id); + + let last_jar_id = self.accounts.get(account_id).map(|jars| jars.last_id); + let product = self.get_product(&ticket.product_id); + + if let Some(pk) = &product.public_key { + let Some(signature) = signature else { + panic_str("Signature is required"); + }; + + let is_time_valid = env::block_timestamp_ms() <= ticket.valid_until.0; + require!(is_time_valid, "Ticket is outdated"); + + let hash = Self::get_ticket_hash(account_id, amount, ticket, last_jar_id); + let is_signature_valid = Self::verify_signature(&signature.0, pk, &hash); + + require!(is_signature_valid, "Not matching signature"); + } + } + + fn get_ticket_hash( + account_id: &AccountId, + amount: TokenAmount, + ticket: &JarTicket, + last_jar_id: Option, + ) -> Vec { + sha256( + Self::get_signature_material( + &env::current_account_id(), + account_id, + &ticket.product_id, + amount, + ticket.valid_until.0, + last_jar_id, + ) + .as_bytes(), + ) + } + + pub(crate) fn get_signature_material( + contract_account_id: &AccountId, + receiver_account_id: &AccountId, + product_id: &ProductId, + amount: TokenAmount, + valid_until: Timestamp, + last_jar_id: Option, + ) -> String { + format!( + "{},{},{},{},{},{}", + contract_account_id, + receiver_account_id, + product_id, + amount, + last_jar_id.map_or_else(String::new, |value| value.to_string()), + valid_until, + ) + } + + fn verify_signature(signature: &[u8], product_public_key: &[u8], ticket_hash: &[u8]) -> bool { + let signature_bytes: &[u8; SIGNATURE_LENGTH] = signature + .try_into() + .unwrap_or_else(|_| panic!("Signature must be {SIGNATURE_LENGTH} bytes")); + + let signature = Signature::from_bytes(signature_bytes); + + let public_key_bytes: &[u8; PUBLIC_KEY_LENGTH] = product_public_key + .try_into() + .unwrap_or_else(|_| panic!("Public key must be {PUBLIC_KEY_LENGTH} bytes")); + + VerifyingKey::from_bytes(public_key_bytes) + .expect("Public key is invalid") + .verify_strict(ticket_hash, &signature) + .is_ok() + } +} diff --git a/contract/src/lib.rs b/contract/src/lib.rs index 334ca34a..809bcff0 100644 --- a/contract/src/lib.rs +++ b/contract/src/lib.rs @@ -10,10 +10,11 @@ use sweat_jar_model::{api::InitApi, jar::JarId, ProductId}; use crate::{ jar::{ - account::versioned::Account, + account::{v2::AccountV2, versioned::Account}, model::{AccountJarsLegacy, Jar}, }, migration::account_jars_non_versioned::AccountJarsNonVersioned, + product::model::v2::ProductV2, }; mod assert; @@ -51,11 +52,13 @@ pub struct Contract { pub manager: AccountId, /// A collection of products, each representing terms for specific deposit jars. - pub products: UnorderedMap, + pub products: UnorderedMap, /// The last jar ID. Is used as nonce in `get_ticket_hash` method. pub last_jar_id: JarId, + pub accounts_v2: LookupMap, + /// A lookup map that associates account IDs with sets of jars owned by each account. pub accounts: LookupMap, @@ -65,7 +68,7 @@ pub struct Contract { /// Cache to make access to products faster /// Is not stored in contract state so it should be always skipped by borsh #[borsh(skip)] - pub products_cache: RefCell>, + pub products_cache: RefCell>, } #[near] diff --git a/contract/src/migration/aggregated_jars.rs b/contract/src/migration/aggregated_jars.rs new file mode 100644 index 00000000..e69de29b diff --git a/contract/src/migration/mod.rs b/contract/src/migration/mod.rs index 0e80529f..8a2074a2 100644 --- a/contract/src/migration/mod.rs +++ b/contract/src/migration/mod.rs @@ -1,4 +1,5 @@ pub mod account_jars_non_versioned; +mod aggregated_jars; pub mod api; pub mod claim_rounding_error; pub mod step_jars; diff --git a/contract/src/product/api.rs b/contract/src/product/api.rs index 9b1acd27..042b69c6 100644 --- a/contract/src/product/api.rs +++ b/contract/src/product/api.rs @@ -7,7 +7,7 @@ use sweat_jar_model::{ use crate::{ event::{emit, ChangeProductPublicKeyData, EnableProductData, EventKind}, - product::model::{Apy, Product, Terms}, + product::model::v2::ProductV2, Base64VecU8, Contract, ContractExt, }; @@ -20,20 +20,7 @@ impl ProductApi for Contract { assert!(self.products.get(&command.id).is_none(), "Product already exists"); - let product: Product = command.into(); - - if product.is_score_product() { - let apy = match product.apy { - Apy::Constant(apy) => apy, - Apy::Downgradable(_) => panic_str("Step based products do not support downgradable APY"), - }; - - assert!(apy.is_zero(), "Step based products do not support constant APY"); - - if let Terms::Fixed(fixed) = &product.terms { - assert!(!fixed.allows_top_up, "Step based products don't support top up"); - } - } + let product: ProductV2 = command.into(); product.assert_fee_amount(); diff --git a/contract/src/product/model/v2.rs b/contract/src/product/model/v2.rs index e96dc5c6..c8b5a2b0 100644 --- a/contract/src/product/model/v2.rs +++ b/contract/src/product/model/v2.rs @@ -124,6 +124,7 @@ pub struct Cap { } impl ProductV2 { + // TODO: should it test total principal? pub(crate) fn assert_cap(&self, amount: TokenAmount) { if self.cap.min > amount || amount > self.cap.max { env::panic_str(&format!( @@ -156,10 +157,9 @@ impl ProductV2 { } pub(crate) trait InterestCalculator { - fn get_interest(&self, account: AccountV2, jar: JarV2) -> (TokenAmount, UDecimal) { - let now = env::block_timestamp_ms(); + fn get_interest(&self, account: &AccountV2, jar: &JarV2) -> (TokenAmount, u64) { let since_date = jar.cache.map(|cache| cache.updated_at); - let apy = self.get_apy(&account); + let apy = self.get_apy(account); let (interest, remainder): (TokenAmount, u64) = jar .deposits @@ -188,6 +188,24 @@ pub(crate) trait InterestCalculator { fn get_interest_calculation_term(&self, last_cached_at: Option, deposit: &Deposit) -> Duration; } +impl InterestCalculator for Terms { + fn get_apy(&self, account: &AccountV2) -> UDecimal { + match self { + Terms::Fixed(terms) => terms.get_apy(account), + Terms::Flexible(terms) => terms.get_apy(account), + Terms::ScoreBased(terms) => terms.get_apy(account), + } + } + + fn get_interest_calculation_term(&self, last_cached_at: Option, deposit: &Deposit) -> Duration { + match self { + Terms::Fixed(terms) => terms.get_interest_calculation_term(last_cached_at, deposit), + Terms::Flexible(terms) => terms.get_interest_calculation_term(last_cached_at, deposit), + Terms::ScoreBased(terms) => terms.get_interest_calculation_term(last_cached_at, deposit), + } + } +} + impl InterestCalculator for FixedProductTerms { fn get_apy(&self, account: &AccountV2) -> UDecimal { self.apy.get_effective(account.is_penalty_applied) @@ -221,7 +239,8 @@ impl InterestCalculator for ScoreBasedProductTerms { fn get_apy(&self, account: &AccountV2) -> UDecimal { let score = account.score.claimable_score(); - self.get_apy(&score, account.is_penalty_applied) + let total_score: Score = score.iter().map(|score| score.min(&self.score_cap)).sum(); + self.base_apy.get_effective(account.is_penalty_applied) + total_score.to_apy() } fn get_interest_calculation_term(&self, last_cached_at: Option, deposit: &Deposit) -> Timestamp { @@ -234,13 +253,6 @@ impl InterestCalculator for ScoreBasedProductTerms { } } -impl ScoreBasedProductTerms { - fn get_apy(&self, score: &[Score], is_penalty_applied: bool) -> UDecimal { - let total_score: Score = score.iter().map(|score| score.min(&self.score_cap)).sum(); - self.base_apy.get_effective(is_penalty_applied) + total_score.to_apy() - } -} - fn get_interest(principal: TokenAmount, apy: UDecimal, term: Duration) -> (TokenAmount, u64) { let term_in_milliseconds: u128 = term.into(); diff --git a/contract/src/score/account_score.rs b/contract/src/score/account_score.rs index 8147b7ff..dc676ad8 100644 --- a/contract/src/score/account_score.rs +++ b/contract/src/score/account_score.rs @@ -46,6 +46,7 @@ impl AccountScore { } /// On claim we need to clear active scores so they aren't claimed twice or more. + // TODO: at least rename pub fn claim_score(&mut self) -> Vec { let today = self.timezone.today(); diff --git a/model/src/claimed_amount_view.rs b/model/src/claimed_amount_view.rs index 67a30321..64447e20 100644 --- a/model/src/claimed_amount_view.rs +++ b/model/src/claimed_amount_view.rs @@ -2,7 +2,7 @@ use near_sdk::{json_types::U128, near}; use crate::{ jar::{AggregatedTokenAmountView, JarId}, - TokenAmount, U32, + ProductId, TokenAmount, U32, }; #[derive(Debug, PartialEq, Clone)] @@ -29,14 +29,14 @@ impl ClaimedAmountView { } } - pub fn add(&mut self, jar_id: JarId, amount: TokenAmount) { + pub fn add(&mut self, product_id: &ProductId, amount: TokenAmount) { match self { ClaimedAmountView::Total(value) => { value.0 += amount; } ClaimedAmountView::Detailed(value) => { value.total.0 += amount; - value.detailed.insert(U32(jar_id), U128(amount)); + value.detailed.insert(product_id.clone(), U128(amount)); } } } diff --git a/model/src/jar.rs b/model/src/jar.rs index 995ed9a8..eddf86c5 100644 --- a/model/src/jar.rs +++ b/model/src/jar.rs @@ -28,7 +28,7 @@ pub struct JarView { #[derive(Debug, Clone, PartialEq)] #[near(serializers=[json])] pub struct AggregatedTokenAmountView { - pub detailed: HashMap, + pub detailed: HashMap, pub total: U128, } From 09e950b8a2e10a2a88c1307cb204f01c75f904e6 Mon Sep 17 00:00:00 2001 From: Vasily Styagov Date: Wed, 9 Oct 2024 19:30:20 +0100 Subject: [PATCH 03/93] refactor penalty --- contract/src/event.rs | 6 ++-- contract/src/jar/account/v2.rs | 27 +++++++++++++- contract/src/penalty/api.rs | 60 +++++++------------------------- contract/src/product/model/v2.rs | 9 +++++ model/src/api.rs | 4 +-- 5 files changed, 54 insertions(+), 52 deletions(-) diff --git a/contract/src/event.rs b/contract/src/event.rs index 20172678..566f0dc9 100644 --- a/contract/src/event.rs +++ b/contract/src/event.rs @@ -91,16 +91,18 @@ pub type RestakeData = (JarId, JarId); #[derive(Debug)] #[near(serializers=[json])] +// TODO: doc change pub struct PenaltyData { - pub id: JarId, + pub account_id: AccountId, pub is_applied: bool, pub timestamp: Timestamp, } #[derive(Debug)] #[near(serializers=[json])] +// TODO: doc change pub struct BatchPenaltyData { - pub jars: Vec, + pub account_ids: Vec, pub is_applied: bool, pub timestamp: Timestamp, } diff --git a/contract/src/jar/account/v2.rs b/contract/src/jar/account/v2.rs index 8c8ed813..8d30b8e2 100644 --- a/contract/src/jar/account/v2.rs +++ b/contract/src/jar/account/v2.rs @@ -8,8 +8,9 @@ use sweat_jar_model::{jar::JarId, ProductId, Timezone, TokenAmount}; use crate::{ common::Timestamp, - jar::model::{AccountJarsLegacy, Deposit, Jar, JarV2, JarV2Companion}, + jar::model::{AccountJarsLegacy, Deposit, Jar, JarCache, JarV2, JarV2Companion}, migration::account_jars_non_versioned::AccountJarsNonVersioned, + product::model::v2::InterestCalculator, score::AccountScore, Contract, }; @@ -34,6 +35,12 @@ pub struct AccountV2Companion { } impl Contract { + pub(crate) fn get_account_mut(&mut self, account_id: &AccountId) -> &mut AccountV2 { + self.accounts_v2 + .get_mut(account_id) + .unwrap_or_else(|| env::panic_str(format!("Account {account_id} is not found").as_str())) + } + pub(crate) fn get_or_create_account_mut(&mut self, account_id: &AccountId) -> &mut AccountV2 { if !self.accounts_v2.contains_key(&account_id) { self.accounts_v2.insert(account_id.clone(), AccountV2::default()); @@ -101,3 +108,21 @@ impl AccountV2 { } } } + +impl Contract { + pub(crate) fn update_cache(&mut self, account: &mut AccountV2) { + let now = env::block_timestamp_ms(); + + for (product_id, jar) in account.jars.iter_mut() { + let product = self.get_product(product_id); + + let (interest, remainder) = product.terms.get_interest(account, jar); + jar.cache = Some(JarCache { + updated_at: now, + interest, + }); + // TODO: adjust remainder + jar.claim_remainder += remainder; + } + } +} diff --git a/contract/src/penalty/api.rs b/contract/src/penalty/api.rs index fb0a90ad..019b0cf9 100644 --- a/contract/src/penalty/api.rs +++ b/contract/src/penalty/api.rs @@ -13,71 +13,37 @@ use crate::{ #[near_bindgen] impl PenaltyApi for Contract { - fn set_penalty(&mut self, account_id: AccountId, jar_id: JarIdView, value: bool) { + fn set_penalty(&mut self, account_id: AccountId, value: bool) { self.assert_manager(); self.migrate_account_if_needed(&account_id); - let jar_id = jar_id.0; - let jar = self.get_jar_internal(&account_id, jar_id); - let product = self.get_product(&jar.product_id).clone(); - let now = env::block_timestamp_ms(); - - assert_penalty_apy(&product.apy); - - self.get_jar_mut_internal(&account_id, jar_id) - .apply_penalty(&product, value, now); + let account = self.get_account_mut(&account_id); + account.is_penalty_applied = value; + self.update_cache(account); emit(ApplyPenalty(PenaltyData { - id: jar_id, + account_id, is_applied: value, - timestamp: now, + timestamp: env::block_timestamp_ms(), })); } - fn batch_set_penalty(&mut self, jars: Vec<(AccountId, Vec)>, value: bool) { + fn batch_set_penalty(&mut self, account_ids: Vec, value: bool) { self.assert_manager(); - let mut applied_jars = vec![]; - - let now = env::block_timestamp_ms(); - - for (account_id, jars) in jars { + for account_id in account_ids.iter() { self.migrate_account_if_needed(&account_id); - let account_jars = self - .accounts - .get_mut(&account_id) - .unwrap_or_else(|| env::panic_str(&format!("Account '{account_id}' doesn't exist"))); - - for jar_id in jars { - let jar_id = jar_id.0; - - let jar = account_jars.get_jar_mut(jar_id); - - let product = self - .products - .get(&jar.product_id) - .unwrap_or_else(|| env::panic_str(&format!("Product '{}' doesn't exist", jar.product_id))); - - assert_penalty_apy(&product.apy); - jar.apply_penalty(&product, value, now); - - applied_jars.push(jar_id); - } + let account = self.get_account_mut(account_id); + account.is_penalty_applied = value; + self.update_cache(account); } emit(BatchApplyPenalty(BatchPenaltyData { - jars: applied_jars, + account_ids, is_applied: value, - timestamp: now, + timestamp: env::block_timestamp_ms(), })); } } - -fn assert_penalty_apy(apy: &Apy) { - match apy { - Apy::Constant(_) => env::panic_str("Penalty is not applicable for constant APY"), - Apy::Downgradable(_) => (), - } -} diff --git a/contract/src/product/model/v2.rs b/contract/src/product/model/v2.rs index c8b5a2b0..aa0f9202 100644 --- a/contract/src/product/model/v2.rs +++ b/contract/src/product/model/v2.rs @@ -11,6 +11,7 @@ use crate::{ model::{Deposit, JarV2}, }, score::AccountScore, + Contract, }; /// The `Product` struct describes the terms of a deposit jar. It can be of Flexible or Fixed type. @@ -284,3 +285,11 @@ impl Apy { } } } + +impl Contract { + pub(crate) fn get_product(&self, product_id: &ProductId) -> ProductV2 { + self.products + .get(product_id) + .unwrap_or_else(|| env::panic_str(format!("Product {product_id} is not found").as_str())) + } +} diff --git a/model/src/api.rs b/model/src/api.rs index fe771b13..5dec6cb6 100644 --- a/model/src/api.rs +++ b/model/src/api.rs @@ -174,7 +174,7 @@ pub trait PenaltyApi { /// # Panics /// /// This method will panic if the jar's associated product has a constant APY rather than a downgradable APY. - fn set_penalty(&mut self, account_id: AccountId, jar_id: JarIdView, value: bool); + fn set_penalty(&mut self, account_id: AccountId, value: bool); /// Batched version of `set_penalty` /// @@ -186,7 +186,7 @@ pub trait PenaltyApi { /// # Panics /// /// This method will panic if the jar's associated product has a constant APY rather than a downgradable APY. - fn batch_set_penalty(&mut self, jars: Vec<(AccountId, Vec)>, value: bool); + fn batch_set_penalty(&mut self, account_ids: Vec, value: bool); } /// The `ProductApi` trait defines methods for managing products within the smart contract. From a271fac10cbeef553bb279a4160cc547b56610d0 Mon Sep 17 00:00:00 2001 From: Vasily Styagov Date: Thu, 10 Oct 2024 19:23:02 +0100 Subject: [PATCH 04/93] refactor withdraw --- contract/src/assert.rs | 11 +- contract/src/claim/api.rs | 2 +- contract/src/event.rs | 2 +- contract/src/jar/model/v2.rs | 2 +- contract/src/product/model/v2.rs | 21 ++ contract/src/withdraw/api.rs | 421 ++++++++++++++----------------- contract/src/withdraw/tests.rs | 4 +- model/src/api.rs | 4 +- model/src/withdraw.rs | 3 +- 9 files changed, 221 insertions(+), 249 deletions(-) diff --git a/contract/src/assert.rs b/contract/src/assert.rs index 8af13bc1..ed740722 100644 --- a/contract/src/assert.rs +++ b/contract/src/assert.rs @@ -1,16 +1,15 @@ use near_sdk::require; use sweat_jar_model::TokenAmount; -use crate::{common::Timestamp, jar::model::Jar, product::model::Product}; +use crate::{ + common::Timestamp, + jar::model::{Jar, JarV2}, +}; -pub(crate) fn assert_not_locked(jar: &Jar) { +pub(crate) fn assert_not_locked(jar: &JarV2) { require!(!jar.is_pending_withdraw, "Another operation on this Jar is in progress"); } pub(crate) fn assert_sufficient_balance(jar: &Jar, amount: TokenAmount) { require!(jar.principal >= amount, "Insufficient balance"); } - -pub(crate) fn assert_is_liquidable(jar: &Jar, product: &Product, now: Timestamp) { - require!(jar.is_liquidable(product, now), "The jar is not mature yet"); -} diff --git a/contract/src/claim/api.rs b/contract/src/claim/api.rs index 34602b50..10447e25 100644 --- a/contract/src/claim/api.rs +++ b/contract/src/claim/api.rs @@ -159,7 +159,7 @@ impl Contract { claimed_amount } else { - let account = self.accounts_v2.get_mut(&account_id).expect("Account is not found"); + let account = self.get_account_mut(&account_id); account.apply(&account_rollback); match claimed_amount { diff --git a/contract/src/event.rs b/contract/src/event.rs index 566f0dc9..e81b9279 100644 --- a/contract/src/event.rs +++ b/contract/src/event.rs @@ -76,7 +76,7 @@ struct SweatJarEvent { pub type ClaimEventItem = (JarId, U128); /// (id, fee, amount) -pub type WithdrawData = (JarId, U128, U128); +pub type WithdrawData = (ProductId, U128, U128); #[derive(Debug)] #[near(serializers=[json])] diff --git a/contract/src/jar/model/v2.rs b/contract/src/jar/model/v2.rs index f3dbc751..773857f8 100644 --- a/contract/src/jar/model/v2.rs +++ b/contract/src/jar/model/v2.rs @@ -88,7 +88,7 @@ impl Deposit { Self { created_at, principal } } - pub(crate) fn is_liquidable(&self, now: Timestamp, term: Duration) -> bool { + pub(crate) fn is_liquid(&self, now: Timestamp, term: Duration) -> bool { now - self.created_at > term } } diff --git a/contract/src/product/model/v2.rs b/contract/src/product/model/v2.rs index aa0f9202..a2ea2757 100644 --- a/contract/src/product/model/v2.rs +++ b/contract/src/product/model/v2.rs @@ -1,5 +1,6 @@ use std::{cmp, iter::Sum}; +use near_contract_standards::non_fungible_token::Token; use near_sdk::{near, require}; use sweat_jar_model::{ProductId, Score, ToAPY, TokenAmount, UDecimal, MS_IN_YEAR}; @@ -124,7 +125,27 @@ pub struct Cap { pub max: TokenAmount, } +impl Terms { + pub(crate) fn allows_early_withdrawal(&self) -> bool { + match self { + Terms::Flexible(_) => true, + _ => false, + } + } +} + impl ProductV2 { + pub(crate) fn calculate_fee(&self, principal: TokenAmount) -> TokenAmount { + if let Some(fee) = self.withdrawal_fee.clone() { + return match fee { + WithdrawalFee::Fix(amount) => *amount, + WithdrawalFee::Percent(percent) => percent * principal, + }; + } + + 0 + } + // TODO: should it test total principal? pub(crate) fn assert_cap(&self, amount: TokenAmount) { if self.cap.min > amount || amount > self.cap.max { diff --git a/contract/src/withdraw/api.rs b/contract/src/withdraw/api.rs index 9bd3d575..33d7da69 100644 --- a/contract/src/withdraw/api.rs +++ b/contract/src/withdraw/api.rs @@ -1,157 +1,125 @@ +use common::test_data; use near_sdk::{ - ext_contract, - json_types::U128, - near_bindgen, + ext_contract, near_bindgen, serde::{Deserialize, Serialize}, PromiseOrValue, }; use sweat_jar_model::{ api::WithdrawApi, - jar::{JarId, JarIdView}, withdraw::{BulkWithdrawView, Fee, WithdrawView}, - TokenAmount, JAR_BATCH_SIZE, + ProductId, TokenAmount, }; -use crate::internal::is_promise_success; +use crate::internal::{assert_gas, is_promise_success}; -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Default)] #[serde(crate = "near_sdk::serde")] -pub struct JarWithdraw { - pub jar: Jar, - pub should_be_closed: bool, - pub amount: u128, - pub fee: Option, +pub struct WithdrawalRequest { + pub product_id: ProductId, + pub amount: TokenAmount, + pub fee: TokenAmount, + pub partition_index: usize, +} + +#[derive(Serialize, Deserialize, Debug, Default)] +#[serde(crate = "near_sdk::serde")] +pub struct BulkWithdrawalRequest { + pub requests: Vec, + pub total_amount: TokenAmount, + pub total_fee: TokenAmount, } #[cfg(not(test))] use crate::ft_interface::FungibleTokenInterface; use crate::{ - assert::{assert_is_liquidable, assert_not_locked, assert_sufficient_balance}, + assert::assert_not_locked, + common, + common::{ + gas_data::{GAS_FOR_BULK_AFTER_WITHDRAW, GAS_FOR_FT_TRANSFER}, + Timestamp, + }, env, event::{emit, EventKind}, - jar::model::Jar, - product::model::WithdrawalFee, - score::AccountScore, - AccountId, Contract, ContractExt, Product, + jar::model::JarV2, + product::model::v2::Terms, + AccountId, Contract, ContractExt, }; -impl Contract { - fn can_be_withdrawn(jar: &Jar, product: &Product, now: u64) -> bool { - !jar.is_pending_withdraw && jar.is_liquidable(product, now) && !jar.is_empty() - } -} - #[allow(dead_code)] // False positive since rust 1.78. It is used from `ext_contract` macro. #[ext_contract(ext_self)] pub trait WithdrawCallbacks { - fn after_withdraw( - &mut self, - account_id: AccountId, - jar_id: JarId, - close_jar: bool, - withdrawn_amount: TokenAmount, - fee: Option, - ) -> WithdrawView; + fn after_withdraw(&mut self, account_id: AccountId, request: WithdrawalRequest) -> WithdrawView; - fn after_bulk_withdraw(&mut self, account_id: AccountId, jars: Vec) -> BulkWithdrawView; + fn after_bulk_withdraw(&mut self, account_id: AccountId, request: BulkWithdrawalRequest) -> BulkWithdrawView; } #[near_bindgen] impl WithdrawApi for Contract { - fn withdraw(&mut self, jar_id: JarIdView, amount: Option) -> PromiseOrValue { + // TODO: doc change + fn withdraw(&mut self, product_id: ProductId) -> PromiseOrValue { let account_id = env::predecessor_account_id(); self.migrate_account_if_needed(&account_id); - let jar = self.get_jar_internal(&account_id, jar_id.0).clone(); + let account = self.get_account_mut(&account_id); - assert_not_locked(&jar); + let jar = account.jars.get_mut(&product_id).expect("No jar for the product"); + assert_not_locked(jar); + jar.lock(); - let amount = amount.map_or(jar.principal, |value| value.0); + // TODO: add method for withdrawal on a single jar + self.update_cache(account); - assert_sufficient_balance(&jar, amount); + let product = self.get_product(&product_id); + let (amount, partition_index) = jar.get_liquid_balance(product.terms, env::block_timestamp_ms()); + let fee = product.calculate_fee(amount); - let now = env::block_timestamp_ms(); - let product = self.get_product(&jar.product_id); - - assert_is_liquidable(&jar, &product, now); - - let score = self - .get_score(&account_id) - .map(AccountScore::claimable_score) - .unwrap_or_default(); - - let mut withdrawn_jar = jar.withdrawn(&score, &product, amount, now); - let close_jar = withdrawn_jar.should_be_closed(&score, &product, now); - - withdrawn_jar.lock(); - *self.get_jar_mut_internal(&jar.account_id, jar.id) = withdrawn_jar; + let request = WithdrawalRequest { + amount, + partition_index, + product_id, + fee, + }; - self.transfer_withdraw(&account_id, amount, &jar, close_jar) + self.transfer_withdraw(&account_id, request) } - fn withdraw_all(&mut self, jars: Option>) -> PromiseOrValue { + fn withdraw_all(&mut self) -> PromiseOrValue { let account_id = env::predecessor_account_id(); self.migrate_account_if_needed(&account_id); let now = env::block_timestamp_ms(); - let Some(account_jars) = self.accounts.get(&account_id) else { - return PromiseOrValue::Value(BulkWithdrawView::default()); - }; - - let score = self - .get_score(&account_id) - .map(AccountScore::claimable_score) - .unwrap_or_default(); - - let jars_filter: Option> = jars.map(|jars| jars.into_iter().map(|j| j.0).collect()); + let account = self.get_account_mut(&account_id); + self.update_cache(account); - let jars: Vec<_> = account_jars + let request: BulkWithdrawalRequest = account .jars - .clone() - .into_iter() - .filter_map(|jar| { - let product = self.get_product(&jar.product_id); - - if !Self::can_be_withdrawn(&jar, &product, now) { - return None; - } - - if let Some(jars_filter) = &jars_filter { - if !jars_filter.contains(&jar.id) { - return None; - } - } - - (jar, product).into() - }) - .take(JAR_BATCH_SIZE) - .collect(); - - let jars: Vec = jars - .into_iter() - .map(|(jar, product)| { - let amount = jar.principal; - - let mut withdrawn_jar = jar.withdrawn(&score, &product, amount, now); - let should_be_closed = withdrawn_jar.should_be_closed(&score, &product, now); - - withdrawn_jar.lock(); - *self.get_jar_mut_internal(&jar.account_id, jar.id) = withdrawn_jar; - - JarWithdraw { - jar, - should_be_closed, - amount, - fee: None, - } - }) - .collect(); - - if jars.is_empty() { + .iter_mut() + .filter(|(_, jar)| !jar.is_pending_withdraw) + .fold( + BulkWithdrawalRequest::default(), + |acc: &mut BulkWithdrawalRequest, (product_id, jar)| { + let product = self.get_product(&product_id); + jar.lock(); + + let (amount, partition_index) = jar.get_liquid_balance(product.terms, now); + let fee = product.calculate_fee(amount); + + acc.requests.push(WithdrawalRequest { + amount, + partition_index, + product_id, + fee, + }); + acc.total_amount += amount; + acc.total_fee += fee; + }, + ); + + if request.requests.is_empty() { return PromiseOrValue::Value(BulkWithdrawView::default()); } - self.transfer_bulk_withdraw(&account_id, jars) + self.transfer_bulk_withdraw(&account_id, request) } } @@ -159,30 +127,34 @@ impl Contract { pub(crate) fn after_withdraw_internal( &mut self, account_id: AccountId, - jar_id: JarId, - close_jar: bool, - withdrawn_amount: TokenAmount, - fee: Option, + request: WithdrawalRequest, is_promise_success: bool, ) -> WithdrawView { - if !is_promise_success { - let jar = self.get_jar_mut_internal(&account_id, jar_id); - jar.principal += withdrawn_amount; - jar.unlock(); + let account = self.get_account_mut(&account_id); + let jar = account + .jars + .get_mut(&request.product_id) + .expect("No jar found for the product"); + jar.unlock(); + if !is_promise_success { return WithdrawView::new(0, None); } - if close_jar { - self.delete_jar(&account_id, jar_id); + if jar.deposits.len() == request.partition_index { + jar.deposits.clear(); } else { - self.get_jar_mut_internal(&account_id, jar_id).unlock(); + jar.deposits.drain(..request.partition_index); + } + + if jar.should_close() { + account.jars.remove(&request.product_id); } - let withdrawal_result = WithdrawView::new(withdrawn_amount, fee); + let withdrawal_result = WithdrawView::new(request.amount, self.wrap_fee(request.fee)); emit(EventKind::Withdraw(( - jar_id, + request.product_id, withdrawal_result.fee, withdrawal_result.withdrawn_amount, ))); @@ -193,38 +165,58 @@ impl Contract { pub(crate) fn after_bulk_withdraw_internal( &mut self, account_id: AccountId, - jars: Vec, + request: BulkWithdrawalRequest, is_promise_success: bool, ) -> BulkWithdrawView { + let account = self.get_account_mut(&account_id); + let mut withdrawal_result = BulkWithdrawView { total_amount: 0.into(), - jars: vec![], + withdrawals: vec![], }; if !is_promise_success { - for withdraw in jars { - let jar = self.get_jar_mut_internal(&account_id, withdraw.jar.id); - jar.principal += withdraw.amount; + for request in request.requests { + let jar = account + .jars + .get_mut(&request.product_id) + .expect("No jar for the product"); jar.unlock(); } + return withdrawal_result; } let mut event_data = vec![]; - for withdraw in jars { - if withdraw.should_be_closed { - self.delete_jar(&account_id, withdraw.jar.id); + for request in request.requests { + let jar = account + .jars + .get_mut(&request.product_id) + .expect("No jar found for the product"); + + jar.unlock(); + + if jar.deposits.len() == request.partition_index { + jar.deposits.clear(); } else { - self.get_jar_mut_internal(&account_id, withdraw.jar.id).unlock(); + jar.deposits.drain(..request.partition_index); } - let jar_result = WithdrawView::new(withdraw.amount, self.make_fee(withdraw.fee)); + if jar.should_close() { + account.jars.remove(&request.product_id); + } - event_data.push((withdraw.jar.id, jar_result.fee, jar_result.withdrawn_amount)); + let deposit_withdrawal = WithdrawView::new(request.amount, self.wrap_fee(request.fee)); - withdrawal_result.total_amount.0 += jar_result.withdrawn_amount.0; - withdrawal_result.jars.push(jar_result); + event_data.push(( + request.product_id, + deposit_withdrawal.fee, + deposit_withdrawal.withdrawn_amount, + )); + + withdrawal_result.total_amount.0 += deposit_withdrawal.withdrawn_amount.0; + withdrawal_result.withdrawals.push(deposit_withdrawal); } emit(EventKind::WithdrawAll(event_data)); @@ -232,23 +224,15 @@ impl Contract { withdrawal_result } - fn get_fee(product: &Product, jar: &Jar) -> Option { - let fee = product.withdrawal_fee.as_ref()?; - - let amount = match fee { - WithdrawalFee::Fix(amount) => *amount, - WithdrawalFee::Percent(percent) => percent * jar.principal, - }; - - amount.into() - } - - fn make_fee(&self, amount: Option) -> Option { - Fee { - beneficiary_id: self.fee_account_id.clone(), - amount: amount?, + fn wrap_fee(&self, amount: TokenAmount) -> Option { + if amount == 0 { + None + } else { + Some(Fee { + beneficiary_id: self.fee_account_id.clone(), + amount, + }) } - .into() } } @@ -258,73 +242,45 @@ impl Contract { fn transfer_withdraw( &mut self, account_id: &AccountId, - amount: TokenAmount, - jar: &Jar, - close_jar: bool, + request: WithdrawalRequest, ) -> PromiseOrValue { - let product = self.get_product(&jar.product_id); - let fee = Self::get_fee(&product, jar); - self.ft_contract() - .ft_transfer(account_id, amount, "withdraw", &self.make_fee(fee)) - .then(Self::after_withdraw_call( - account_id.clone(), - jar.id, - close_jar, - amount, - &self.make_fee(fee), - )) + .ft_transfer(account_id, request.amount, "withdraw", &self.wrap_fee(request.fee)) + .then(Self::after_withdraw_call(account_id.clone(), request)) .into() } fn transfer_bulk_withdraw( &mut self, account_id: &AccountId, - jars: Vec, + request: BulkWithdrawalRequest, ) -> PromiseOrValue { - let total_fee: TokenAmount = jars - .iter() - .filter_map(|j| { - let product = self.get_product(&j.jar.product_id); - Self::get_fee(&product, &j.jar) - }) - .sum(); - - let total_fee = match total_fee { - 0 => None, - _ => self.make_fee(total_fee.into()), - }; - - let total_amount = jars.iter().map(|j| j.amount).sum(); - - crate::internal::assert_gas( - crate::common::gas_data::GAS_FOR_FT_TRANSFER.as_gas() - + crate::common::gas_data::GAS_FOR_BULK_AFTER_WITHDRAW.as_gas(), - || format!("transfer_bulk_withdraw. Number of jars: {}", jars.len()), + assert_gas( + GAS_FOR_FT_TRANSFER.as_gas() + GAS_FOR_BULK_AFTER_WITHDRAW.as_gas(), + || "Not enough gas to finish withdrawal", ); self.ft_contract() - .ft_transfer(account_id, total_amount, "bulk_withdraw", &total_fee) - .then(Self::after_bulk_withdraw_call(account_id.clone(), jars)) + .ft_transfer( + account_id, + request.total_amount, + "bulk_withdraw", + &self.wrap_fee(request.total_fee), + ) + .then(Self::after_bulk_withdraw_call(account_id.clone(), request)) .into() } - fn after_withdraw_call( - account_id: AccountId, - jar_id: JarId, - close_jar: bool, - withdrawn_balance: TokenAmount, - fee: &Option, - ) -> near_sdk::Promise { + fn after_withdraw_call(account_id: AccountId, request: WithdrawalRequest) -> near_sdk::Promise { ext_self::ext(env::current_account_id()) - .with_static_gas(crate::common::gas_data::GAS_FOR_AFTER_WITHDRAW) - .after_withdraw(account_id, jar_id, close_jar, withdrawn_balance, fee.clone()) + .with_static_gas(common::gas_data::GAS_FOR_AFTER_WITHDRAW) + .after_withdraw(account_id, request) } - fn after_bulk_withdraw_call(account_id: AccountId, jars: Vec) -> near_sdk::Promise { + fn after_bulk_withdraw_call(account_id: AccountId, request: BulkWithdrawalRequest) -> near_sdk::Promise { ext_self::ext(env::current_account_id()) - .with_static_gas(crate::common::gas_data::GAS_FOR_BULK_AFTER_WITHDRAW) - .after_bulk_withdraw(account_id, jars) + .with_static_gas(GAS_FOR_BULK_AFTER_WITHDRAW) + .after_bulk_withdraw(account_id, request) } } @@ -333,21 +289,9 @@ impl Contract { fn transfer_withdraw( &mut self, account_id: &AccountId, - amount: TokenAmount, - jar: &Jar, - close_jar: bool, + request: WithdrawalRequest, ) -> PromiseOrValue { - let product = self.get_product(&jar.product_id); - let fee = Self::get_fee(&product, jar); - - let withdrawn = self.after_withdraw_internal( - account_id.clone(), - jar.id, - close_jar, - amount, - self.make_fee(fee), - crate::common::test_data::get_test_future_success(), - ); + let withdrawn = self.after_withdraw_internal(account_id.clone(), request, test_data::get_test_future_success()); PromiseOrValue::Value(withdrawn) } @@ -355,42 +299,49 @@ impl Contract { fn transfer_bulk_withdraw( &mut self, account_id: &AccountId, - jars: Vec, + request: BulkWithdrawalRequest, ) -> PromiseOrValue { - let withdrawn = self.after_bulk_withdraw_internal( - account_id.clone(), - jars, - crate::common::test_data::get_test_future_success(), - ); + let withdrawn = + self.after_bulk_withdraw_internal(account_id.clone(), request, test_data::get_test_future_success()); PromiseOrValue::Value(withdrawn) } } +impl JarV2 { + fn get_liquid_balance(&self, terms: &Terms, now: Timestamp) -> (TokenAmount, usize) { + if terms.allows_early_withdrawal() { + let sum = self.deposits.iter().map(|deposit| deposit.principal).sum(); + let partition_index = self.deposits.len(); + + (sum, partition_index) + } else { + let partition_index = self.deposits.partition_point(|deposit| deposit.is_liquid(now, todo!())); + + let sum = self.deposits[..partition_index] + .iter() + .map(|deposit| deposit.principal) + .sum(); + + (sum, partition_index) + } + } + + fn should_close(&self) -> bool { + self.deposits.is_empty() && self.cache.map_or(true, |cache| cache.interest == 0) + } +} + #[near_bindgen] #[mutants::skip] // Covered by integration tests impl WithdrawCallbacks for Contract { #[private] - fn after_withdraw( - &mut self, - account_id: AccountId, - jar_id: JarId, - close_jar: bool, - withdrawn_amount: TokenAmount, - fee: Option, - ) -> WithdrawView { - self.after_withdraw_internal( - account_id, - jar_id, - close_jar, - withdrawn_amount, - fee, - is_promise_success(), - ) + fn after_withdraw(&mut self, account_id: AccountId, request: WithdrawalRequest) -> WithdrawView { + self.after_withdraw_internal(account_id, request) } #[private] - fn after_bulk_withdraw(&mut self, account_id: AccountId, jars: Vec) -> BulkWithdrawView { - self.after_bulk_withdraw_internal(account_id, jars, is_promise_success()) + fn after_bulk_withdraw(&mut self, account_id: AccountId, request: BulkWithdrawalRequest) -> BulkWithdrawView { + self.after_bulk_withdraw_internal(account_id, request, is_promise_success()) } } diff --git a/contract/src/withdraw/tests.rs b/contract/src/withdraw/tests.rs index 418e9893..6ef8e75c 100644 --- a/contract/src/withdraw/tests.rs +++ b/contract/src/withdraw/tests.rs @@ -11,7 +11,7 @@ use crate::{ jar::model::Jar, product::model::{Apy, Product, WithdrawalFee}, test_utils::{admin, expect_panic, UnwrapPromise, PRINCIPAL}, - withdraw::api::JarWithdraw, + withdraw::api::WithdrawalRequest, }; fn prepare_jar(product: &Product) -> (AccountId, Jar, Context) { @@ -281,7 +281,7 @@ fn test_failed_bulk_withdraw_internal() { let withdraw = contract.after_bulk_withdraw_internal( jar.account_id.clone(), - vec![JarWithdraw { + vec![WithdrawalRequest { jar: jar.clone(), should_be_closed: true, amount: jar.principal, diff --git a/model/src/api.rs b/model/src/api.rs index 5dec6cb6..35f00b60 100644 --- a/model/src/api.rs +++ b/model/src/api.rs @@ -265,10 +265,10 @@ pub trait WithdrawApi { /// - If the caller is not the owner of the specified jar. /// - If the withdrawal amount exceeds the available balance in the jar. /// - If attempting to withdraw from a Fixed jar that is not yet mature. - fn withdraw(&mut self, jar_id: JarIdView, amount: Option) -> ::near_sdk::PromiseOrValue; + fn withdraw(&mut self, product_id: ProductId) -> ::near_sdk::PromiseOrValue; /// Withdraws all jars for user, or only specified list of jars if `jars` argument is `Some` - fn withdraw_all(&mut self, jars: Option>) -> ::near_sdk::PromiseOrValue; + fn withdraw_all(&mut self) -> ::near_sdk::PromiseOrValue; } #[make_integration_version] diff --git a/model/src/withdraw.rs b/model/src/withdraw.rs index fce63e19..98bd3042 100644 --- a/model/src/withdraw.rs +++ b/model/src/withdraw.rs @@ -15,9 +15,10 @@ pub struct WithdrawView { #[derive(Debug, Default)] #[near(serializers=[json])] +// TODO: doc change pub struct BulkWithdrawView { pub total_amount: U128, - pub jars: Vec, + pub withdrawals: Vec, } impl WithdrawView { From f68e6b24fe69c4da04dc2f343ba847f8dd4e1c64 Mon Sep 17 00:00:00 2001 From: Vasily Styagov Date: Thu, 10 Oct 2024 19:57:52 +0100 Subject: [PATCH 05/93] some clean --- contract/src/claim/api.rs | 2 +- contract/src/jar/account/v2.rs | 15 +++++----- contract/src/withdraw/api.rs | 54 +++++++++++++--------------------- 3 files changed, 28 insertions(+), 43 deletions(-) diff --git a/contract/src/claim/api.rs b/contract/src/claim/api.rs index 10447e25..ceb9f35e 100644 --- a/contract/src/claim/api.rs +++ b/contract/src/claim/api.rs @@ -149,7 +149,7 @@ impl Contract { let jars = account_rollback.jars.expect("Jars are required in rollback account"); for (product_id, _) in jars { - let jar = account.jars.get_mut(&product_id).expect("Jar is not found"); + let jar = account.get_jar_mut(&product_id); jar.unlock(); // TODO: check if should delete jar diff --git a/contract/src/jar/account/v2.rs b/contract/src/jar/account/v2.rs index 8d30b8e2..57fb2639 100644 --- a/contract/src/jar/account/v2.rs +++ b/contract/src/jar/account/v2.rs @@ -1,5 +1,6 @@ use std::{ collections::HashMap, + fmt::format, ops::{Deref, DerefMut}, }; @@ -51,6 +52,12 @@ impl Contract { } impl AccountV2 { + pub(crate) fn get_jar_mut(&mut self, product_id: &ProductId) -> &mut JarV2 { + self.jars + .get_mut(&product_id) + .unwrap_or_else(|| env::panic_str(format!("Jar for product {product_id} is not found").as_str())) + } + pub(crate) fn deposit(&mut self, product_id: &ProductId, principal: TokenAmount) { let deposit = Deposit::new(env::block_timestamp_ms(), principal); @@ -64,14 +71,6 @@ impl AccountV2 { } } - pub(crate) fn clean_up_jars(&mut self, product_id: &ProductId) { - if let Some(jar) = self.jars.get_mut(product_id) { - if let Some(last_index_to_remove) = jar.deposits.iter().position(|deposit| deposit.principal != 0) { - jar.deposits.drain(0..=last_index_to_remove); - } - } - } - pub(crate) fn try_set_timezone(&mut self, timezone: Option) { match (timezone, self.score.borrow_mut()) { // Time zone already set. No actions required. diff --git a/contract/src/withdraw/api.rs b/contract/src/withdraw/api.rs index 33d7da69..852f071f 100644 --- a/contract/src/withdraw/api.rs +++ b/contract/src/withdraw/api.rs @@ -40,7 +40,7 @@ use crate::{ }, env, event::{emit, EventKind}, - jar::model::JarV2, + jar::{account::v2::AccountV2, model::JarV2}, product::model::v2::Terms, AccountId, Contract, ContractExt, }; @@ -62,7 +62,7 @@ impl WithdrawApi for Contract { let account = self.get_account_mut(&account_id); - let jar = account.jars.get_mut(&product_id).expect("No jar for the product"); + let jar = account.get_jar_mut(&product_id); assert_not_locked(jar); jar.lock(); @@ -131,25 +131,14 @@ impl Contract { is_promise_success: bool, ) -> WithdrawView { let account = self.get_account_mut(&account_id); - let jar = account - .jars - .get_mut(&request.product_id) - .expect("No jar found for the product"); + let jar = account.get_jar_mut(&request.product_id); jar.unlock(); if !is_promise_success { return WithdrawView::new(0, None); } - if jar.deposits.len() == request.partition_index { - jar.deposits.clear(); - } else { - jar.deposits.drain(..request.partition_index); - } - - if jar.should_close() { - account.jars.remove(&request.product_id); - } + clean_up(&request, account, jar); let withdrawal_result = WithdrawView::new(request.amount, self.wrap_fee(request.fee)); @@ -177,10 +166,7 @@ impl Contract { if !is_promise_success { for request in request.requests { - let jar = account - .jars - .get_mut(&request.product_id) - .expect("No jar for the product"); + let jar = account.get_jar_mut(&request.product_id); jar.unlock(); } @@ -190,22 +176,10 @@ impl Contract { let mut event_data = vec![]; for request in request.requests { - let jar = account - .jars - .get_mut(&request.product_id) - .expect("No jar found for the product"); - + let jar = account.get_jar_mut(&request.product_id); jar.unlock(); - if jar.deposits.len() == request.partition_index { - jar.deposits.clear(); - } else { - jar.deposits.drain(..request.partition_index); - } - - if jar.should_close() { - account.jars.remove(&request.product_id); - } + clean_up(&request, account, jar); let deposit_withdrawal = WithdrawView::new(request.amount, self.wrap_fee(request.fee)); @@ -236,6 +210,18 @@ impl Contract { } } +fn clean_up(request: &WithdrawalRequest, account: &mut AccountV2, jar: &mut JarV2) { + if jar.deposits.len() == request.partition_index { + jar.deposits.clear(); + } else { + jar.deposits.drain(..request.partition_index); + } + + if jar.should_close() { + account.jars.remove(&request.product_id); + } +} + #[cfg(not(test))] #[mutants::skip] // Covered by integration tests impl Contract { @@ -337,7 +323,7 @@ impl JarV2 { impl WithdrawCallbacks for Contract { #[private] fn after_withdraw(&mut self, account_id: AccountId, request: WithdrawalRequest) -> WithdrawView { - self.after_withdraw_internal(account_id, request) + self.after_withdraw_internal(account_id, request, is_promise_success()) } #[private] From a6b7ed6b373c779a54b2c863da13334371270498 Mon Sep 17 00:00:00 2001 From: Vasily Styagov Date: Thu, 10 Oct 2024 23:30:30 +0100 Subject: [PATCH 06/93] refactor restake --- contract/src/jar/account/v2.rs | 6 ++ contract/src/jar/api.rs | 103 ++++++++------------------------- contract/src/jar/model/v2.rs | 23 ++++++++ contract/src/withdraw/api.rs | 30 +--------- model/src/api.rs | 4 +- 5 files changed, 55 insertions(+), 111 deletions(-) diff --git a/contract/src/jar/account/v2.rs b/contract/src/jar/account/v2.rs index 57fb2639..597cda7c 100644 --- a/contract/src/jar/account/v2.rs +++ b/contract/src/jar/account/v2.rs @@ -36,6 +36,12 @@ pub struct AccountV2Companion { } impl Contract { + pub(crate) fn get_account(&mut self, account_id: &AccountId) -> &AccountV2 { + self.accounts_v2 + .get(account_id) + .unwrap_or_else(|| env::panic_str(format!("Account {account_id} is not found").as_str())) + } + pub(crate) fn get_account_mut(&mut self, account_id: &AccountId) -> &mut AccountV2 { self.accounts_v2 .get_mut(account_id) diff --git a/contract/src/jar/api.rs b/contract/src/jar/api.rs index 708db296..a7272ad7 100644 --- a/contract/src/jar/api.rs +++ b/contract/src/jar/api.rs @@ -4,7 +4,7 @@ use near_sdk::{env, env::panic_str, json_types::U128, near_bindgen, require, Acc use sweat_jar_model::{ api::JarApi, jar::{AggregatedInterestView, AggregatedTokenAmountView, JarId, JarIdView, JarView}, - TokenAmount, JAR_BATCH_SIZE, U32, + ProductId, TokenAmount, JAR_BATCH_SIZE, U32, }; use crate::{ @@ -15,56 +15,30 @@ use crate::{ }; impl Contract { - fn can_be_restaked(&self, jar: &Jar, now: u64) -> bool { - let product = self.get_product(&jar.product_id); - !jar.is_empty() && product.is_enabled && product.allows_restaking() && jar.is_liquidable(&product, now) - } + fn restake_internal(&mut self, product_id: &ProductId) { + let product = self.get_product(product_id); + require!(product.is_enabled, "The product is disabled"); - fn restake_internal(&mut self, jar_id: JarIdView) -> (JarId, JarView) { - let jar_id = jar_id.0; let account_id = env::predecessor_account_id(); - - let restaked_jar_id = self.increment_and_get_last_jar_id(); - - let jar = self.get_jar_internal(&account_id, jar_id); - - let product = self.get_product(&jar.product_id); - - require!(product.allows_restaking(), "The product doesn't support restaking"); - require!(product.is_enabled, "The product is disabled"); + let account = self.get_account_mut(&account_id); + let jar = account.get_jar_mut(product_id); let now = env::block_timestamp_ms(); - require!(jar.is_liquidable(&product, now), "The jar is not mature yet"); - require!(!jar.is_empty(), "The jar is empty, nothing to restake"); + let (amount, partition_index) = jar.get_liquid_balance(product.terms, now); - let principal = jar.principal; - - let new_jar = Jar::create( - restaked_jar_id, - jar.account_id.clone(), - jar.product_id.clone(), - principal, - now, - ); - - let score = self - .get_score(&account_id) - .map(AccountScore::claimable_score) - .unwrap_or_default(); + require!(amount > 0, "Nothing to restake"); - let withdraw_jar = jar.withdrawn(&score, &product, principal, now); - let should_be_closed = withdraw_jar.should_be_closed(&score, &product, now); + // TODO: use update for a single jar + self.update_cache(account); - if should_be_closed { - self.delete_jar(&withdraw_jar.account_id, withdraw_jar.id); + // TODO: extract method and use in `clean_up()` + if partition_index == jar.deposits.len() { + jar.deposits.clear(); } else { - let jar_id = withdraw_jar.id; - *self.get_jar_mut_internal(&account_id, jar_id) = withdraw_jar; + jar.deposits.drain(..partition_index); } - self.add_new_jar(&account_id, new_jar.clone()); - - (jar_id, new_jar.into()) + account.deposit(product_id, amount); } } @@ -174,55 +148,24 @@ impl JarApi for Contract { } } - // TODO: add v2 support - fn restake(&mut self, jar_id: JarIdView) -> JarView { + fn restake(&mut self, product_id: ProductId) { self.migrate_account_if_needed(&env::predecessor_account_id()); - let (old_id, jar) = self.restake_internal(jar_id); - - emit(EventKind::Restake((old_id, jar.id.0))); + self.restake_internal(&product_id); - jar + // TODO: add event logging } - fn restake_all(&mut self, jars: Option>) -> Vec { + fn restake_all(&mut self, product_ids: Option>) { let account_id = env::predecessor_account_id(); self.migrate_account_if_needed(&account_id); - let now = env::block_timestamp_ms(); - - let jars_filter: Option> = jars.map(|jars| jars.into_iter().map(|j| j.0).collect()); - - let mut jars: Vec = self - .accounts - .get(&account_id) - .unwrap_or_else(|| { - panic_str(&format!("Jars for account {account_id} don't exist")); - }) - .jars - .iter() - .filter(|j| self.can_be_restaked(j, now)) - .take(JAR_BATCH_SIZE) - .cloned() - .collect(); - - if let Some(jars_filter) = jars_filter { - jars.retain(|jar| jars_filter.contains(&jar.id)); - } - - let mut result = vec![]; - - let mut event_data = vec![]; - - for jar in &jars { - let (old_id, restaked) = self.restake_internal(jar.id.into()); - event_data.push((old_id, restaked.id.0)); - result.push(restaked); + let product_ids = product_ids.unwrap_or_else(|| self.get_account(&account_id).jars.keys().collect()); + for product_id in product_ids { + self.restake_internal(&product_id); } - emit(EventKind::RestakeAll(event_data)); - - result + // TODO: add event logging } // TODO: add v2 support diff --git a/contract/src/jar/model/v2.rs b/contract/src/jar/model/v2.rs index 773857f8..d5285ab9 100644 --- a/contract/src/jar/model/v2.rs +++ b/contract/src/jar/model/v2.rs @@ -4,6 +4,7 @@ use sweat_jar_model::{TokenAmount, UDecimal}; use crate::{ common::{Duration, Timestamp}, jar::model::JarCache, + product::model::v2::Terms, }; /// The `Jar` struct represents a deposit jar within the smart contract. @@ -35,6 +36,28 @@ pub struct Deposit { } impl JarV2 { + pub(crate) fn get_liquid_balance(&self, terms: &Terms, now: Timestamp) -> (TokenAmount, usize) { + if terms.allows_early_withdrawal() { + let sum = self.deposits.iter().map(|deposit| deposit.principal).sum(); + let partition_index = self.deposits.len(); + + (sum, partition_index) + } else { + let partition_index = self.deposits.partition_point(|deposit| deposit.is_liquid(now, todo!())); + + let sum = self.deposits[..partition_index] + .iter() + .map(|deposit| deposit.principal) + .sum(); + + (sum, partition_index) + } + } + + pub(crate) fn should_close(&self) -> bool { + self.deposits.is_empty() && self.cache.map_or(true, |cache| cache.interest == 0) + } + pub(crate) fn lock(&mut self) -> &mut Self { self.is_pending_withdraw = true; diff --git a/contract/src/withdraw/api.rs b/contract/src/withdraw/api.rs index 852f071f..876a44d6 100644 --- a/contract/src/withdraw/api.rs +++ b/contract/src/withdraw/api.rs @@ -34,14 +34,10 @@ use crate::ft_interface::FungibleTokenInterface; use crate::{ assert::assert_not_locked, common, - common::{ - gas_data::{GAS_FOR_BULK_AFTER_WITHDRAW, GAS_FOR_FT_TRANSFER}, - Timestamp, - }, + common::gas_data::{GAS_FOR_BULK_AFTER_WITHDRAW, GAS_FOR_FT_TRANSFER}, env, event::{emit, EventKind}, jar::{account::v2::AccountV2, model::JarV2}, - product::model::v2::Terms, AccountId, Contract, ContractExt, }; @@ -294,30 +290,6 @@ impl Contract { } } -impl JarV2 { - fn get_liquid_balance(&self, terms: &Terms, now: Timestamp) -> (TokenAmount, usize) { - if terms.allows_early_withdrawal() { - let sum = self.deposits.iter().map(|deposit| deposit.principal).sum(); - let partition_index = self.deposits.len(); - - (sum, partition_index) - } else { - let partition_index = self.deposits.partition_point(|deposit| deposit.is_liquid(now, todo!())); - - let sum = self.deposits[..partition_index] - .iter() - .map(|deposit| deposit.principal) - .sum(); - - (sum, partition_index) - } - } - - fn should_close(&self) -> bool { - self.deposits.is_empty() && self.cache.map_or(true, |cache| cache.interest == 0) - } -} - #[near_bindgen] #[mutants::skip] // Covered by integration tests impl WithdrawCallbacks for Contract { diff --git a/model/src/api.rs b/model/src/api.rs index 35f00b60..2105985a 100644 --- a/model/src/api.rs +++ b/model/src/api.rs @@ -133,10 +133,10 @@ pub trait JarApi { /// - If the product of the original jar does not support restaking. /// - If the function is called by an account other than the owner of the original jar. /// - If the original jar is not yet mature. - fn restake(&mut self, jar_id: JarIdView) -> JarView; + fn restake(&mut self, product_id: ProductId); /// Restakes all jars for user, or only specified list of jars if `jars` argument is `Some` - fn restake_all(&mut self, jars: Option>) -> Vec; + fn restake_all(&mut self, product_ids: Option>); fn unlock_jars_for_account(&mut self, account_id: AccountId); } From a900fbc2761cf1a0fd9fa2f7890af5a3508a0e21 Mon Sep 17 00:00:00 2001 From: Vasily Styagov Date: Fri, 11 Oct 2024 19:58:29 +0100 Subject: [PATCH 07/93] refactor view methods --- contract/src/internal.rs | 59 ++++----- contract/src/jar/account/v2.rs | 43 +++++-- contract/src/jar/api.rs | 198 ++++++++++++++----------------- contract/src/jar/model/common.rs | 159 +++++++++++++++---------- contract/src/jar/model/v2.rs | 8 ++ contract/src/jar/view.rs | 35 ++++-- contract/src/product/model/v2.rs | 14 ++- contract/src/withdraw/api.rs | 11 +- model/src/api.rs | 50 -------- model/src/jar.rs | 9 +- 10 files changed, 283 insertions(+), 303 deletions(-) diff --git a/contract/src/internal.rs b/contract/src/internal.rs index 45549867..969e9dd5 100644 --- a/contract/src/internal.rs +++ b/contract/src/internal.rs @@ -6,7 +6,15 @@ use sweat_jar_model::{ ProductId, }; -use crate::{env, jar::model::Jar, product::model::v2::ProductV2, AccountId, Contract, Product}; +use crate::{ + env, + jar::{ + account::{v1::AccountV1, versioned::Account}, + model::Jar, + }, + product::model::v2::ProductV2, + AccountId, Contract, +}; impl Contract { pub(crate) fn assert_manager(&self) { @@ -32,36 +40,29 @@ impl Contract { self.last_jar_id } - pub(crate) fn account_jars(&self, account_id: &AccountId) -> Vec { + pub(crate) fn account_jars(&self, account_id: &AccountId) -> Option> { // TODO: Remove after complete migration and return '&[Jar]` if let Some(record) = self.account_jars_v1.get(account_id) { - return record.jars.iter().map(|j| j.clone().into()).collect(); + return Some(record.jars.iter().map(|j| j.clone().into()).collect()); } if let Some(record) = self.account_jars_non_versioned.get(account_id) { - return record.jars.clone(); + return Some(record.jars.clone()); } - self.accounts - .get(account_id) - .map_or(vec![], |record| record.jars.clone()) + self.accounts.get(account_id).map(|record| record.jars.clone()) } - // TODO: Restore previous version after V2 migration - pub(crate) fn account_jars_with_ids(&self, account_id: &AccountId, ids: &[JarIdView]) -> Vec { - // iterates once over jars and once over ids - let mut jars: HashMap = self - .account_jars(account_id) - .into_iter() - .map(|jar| (jar.id, jar)) - .collect(); - - ids.iter() - .map(|id| { - jars.remove(&id.0) - .unwrap_or_else(|| env::panic_str(&format!("Jar with id: '{}' doesn't exist", id.0))) - }) - .collect() + pub(crate) fn get_account_legacy(&self, account_id: &AccountId) -> Option<&Account> { + if let Some(record) = self.account_jars_v1.get(account_id) { + return Account::from(record).into(); + } + + if let Some(record) = self.account_jars_non_versioned.get(account_id) { + return Account::from(record).into(); + } + + self.accounts.get(account_id) } pub(crate) fn add_new_jar(&mut self, account_id: &AccountId, jar: Jar) { @@ -70,20 +71,6 @@ impl Contract { jars.last_id = jar.id; jars.push(jar); } - - // UnorderedMap doesn't have cache and deserializes `Product` on each get - // This cached getter significantly reduces gas usage - pub fn get_product(&self, product_id: &ProductId) -> ProductV2 { - self.products_cache - .borrow_mut() - .entry(product_id.clone()) - .or_insert_with(|| { - self.products - .get(product_id) - .unwrap_or_else(|| env::panic_str(&format!("Product '{product_id}' doesn't exist"))) - }) - .clone() - } } pub(crate) fn assert_gas(gas_needed: u64, error: impl FnOnce() -> Message) { diff --git a/contract/src/jar/account/v2.rs b/contract/src/jar/account/v2.rs index 597cda7c..5d4368da 100644 --- a/contract/src/jar/account/v2.rs +++ b/contract/src/jar/account/v2.rs @@ -11,7 +11,7 @@ use crate::{ common::Timestamp, jar::model::{AccountJarsLegacy, Deposit, Jar, JarCache, JarV2, JarV2Companion}, migration::account_jars_non_versioned::AccountJarsNonVersioned, - product::model::v2::InterestCalculator, + product::model::v2::{InterestCalculator, ProductV2}, score::AccountScore, Contract, }; @@ -36,7 +36,11 @@ pub struct AccountV2Companion { } impl Contract { - pub(crate) fn get_account(&mut self, account_id: &AccountId) -> &AccountV2 { + pub(crate) fn try_get_account(&self, account_id: &AccountId) -> Option<&AccountV2> { + self.accounts_v2.get(account_id) + } + + pub(crate) fn get_account(&self, account_id: &AccountId) -> &AccountV2 { self.accounts_v2 .get(account_id) .unwrap_or_else(|| env::panic_str(format!("Account {account_id} is not found").as_str())) @@ -66,7 +70,11 @@ impl AccountV2 { pub(crate) fn deposit(&mut self, product_id: &ProductId, principal: TokenAmount) { let deposit = Deposit::new(env::block_timestamp_ms(), principal); + self.push(product_id, deposit); + } + // TODO: refactor, move to some container + pub(crate) fn push(&mut self, product_id: &ProductId, deposit: Deposit) { if let Some(jar) = self.jars.get_mut(product_id) { jar.deposits.push(deposit); } else { @@ -115,19 +123,30 @@ impl AccountV2 { } impl Contract { - pub(crate) fn update_cache(&mut self, account: &mut AccountV2) { + pub(crate) fn update_account_cache(&mut self, account: &mut AccountV2) { let now = env::block_timestamp_ms(); for (product_id, jar) in account.jars.iter_mut() { - let product = self.get_product(product_id); - - let (interest, remainder) = product.terms.get_interest(account, jar); - jar.cache = Some(JarCache { - updated_at: now, - interest, - }); - // TODO: adjust remainder - jar.claim_remainder += remainder; + let product = &self.get_product(product_id); + jar.update_cache(account, product, now); } } + + pub(crate) fn update_jar_cache(&mut self, account: &mut AccountV2, product_id: &ProductId) { + let product = &self.get_product(product_id); + let jar = account.get_jar_mut(product_id); + jar.update_cache(account, product, env::block_timestamp_ms()); + } +} + +impl JarV2 { + fn update_cache(&mut self, account: &AccountV2, product: &ProductV2, now: Timestamp) { + let (interest, remainder) = product.terms.get_interest(account, self); + self.cache = Some(JarCache { + updated_at: now, + interest, + }); + // TODO: adjust remainder + self.claim_remainder += remainder; + } } diff --git a/contract/src/jar/api.rs b/contract/src/jar/api.rs index a7272ad7..874e9604 100644 --- a/contract/src/jar/api.rs +++ b/contract/src/jar/api.rs @@ -9,133 +9,114 @@ use sweat_jar_model::{ use crate::{ event::{emit, EventKind}, - jar::model::Jar, + jar::{ + account::{v1::AccountV1, v2::AccountV2}, + model::{Deposit, Jar, JarV2}, + view::DetailedJarV2, + }, + product::model::v2::{InterestCalculator, ProductV2}, score::AccountScore, Contract, ContractExt, JarsStorage, }; impl Contract { - fn restake_internal(&mut self, product_id: &ProductId) { - let product = self.get_product(product_id); + fn restake_internal(&mut self, product: &ProductV2) { require!(product.is_enabled, "The product is disabled"); let account_id = env::predecessor_account_id(); let account = self.get_account_mut(&account_id); - let jar = account.get_jar_mut(product_id); + let jar = account.get_jar_mut(&product.id); let now = env::block_timestamp_ms(); - let (amount, partition_index) = jar.get_liquid_balance(product.terms, now); + let (amount, partition_index) = jar.get_liquid_balance(&product.terms, now); require!(amount > 0, "Nothing to restake"); - // TODO: use update for a single jar - self.update_cache(account); - - // TODO: extract method and use in `clean_up()` - if partition_index == jar.deposits.len() { - jar.deposits.clear(); - } else { - jar.deposits.drain(..partition_index); - } - - account.deposit(product_id, amount); + self.update_jar_cache(account, &product.id); + jar.clean_up_deposits(partition_index); + account.deposit(&product.id, amount); } } #[near_bindgen] impl JarApi for Contract { - // TODO: restore previous version after V2 migration - // TODO: add v2 support - #[mutants::skip] - fn get_jar(&self, account_id: AccountId, jar_id: JarIdView) -> JarView { - if let Some(record) = self.account_jars_v1.get(&account_id) { - let jar: Jar = record + fn get_jars_for_account(&self, account_id: AccountId) -> Vec { + if let Some(account) = self.try_get_account(&account_id) { + return account .jars .iter() - .find(|jar| jar.id == jar_id.0) - .unwrap_or_else(|| env::panic_str(&format!("Jar with id: {} doesn't exist", jar_id.0))) - .clone() - .into(); - - return jar.into(); + .flat_map(|(product_id, jar)| DetailedJarV2(product_id.clone(), jar.clone()).into()) + .collect(); } - if let Some(record) = self.account_jars_non_versioned.get(&account_id) { - let jar: Jar = record - .jars - .iter() - .find(|jar| jar.id == jar_id.0) - .unwrap_or_else(|| env::panic_str(&format!("Jar with id: {} doesn't exist", jar_id.0))) - .clone(); - - return jar.into(); + if let Some(jars) = self.account_jars(&account_id) { + return jars.iter().map(Into::into).collect(); } - self.accounts - .get(&account_id) - .unwrap_or_else(|| panic_str(&format!("Account '{account_id}' doesn't exist"))) - .get_jar(jar_id.0) - .into() + vec![] } // TODO: add v2 support - fn get_jars_for_account(&self, account_id: AccountId) -> Vec { - self.account_jars(&account_id).iter().map(Into::into).collect() + fn get_total_interest(&self, account_id: AccountId) -> AggregatedInterestView { + if let Some(account) = self.try_get_account(&account_id) { + return account.get_total_interest(); + } + + if let Some(account) = self.get_account_legacy(&account_id) { + return AccountV2::from(account).get_total_interest(); + } + + AggregatedInterestView::default() } - // TODO: add v2 support - fn get_total_principal(&self, account_id: AccountId) -> AggregatedTokenAmountView { - self.get_principal( - self.account_jars(&account_id).iter().map(|a| U32(a.id)).collect(), - account_id, - ) + fn restake(&mut self, product_id: ProductId) { + self.migrate_account_if_needed(&env::predecessor_account_id()); + + self.restake_internal(&self.get_product(&product_id)); + + // TODO: add event logging } - // TODO: add v2 support - fn get_principal(&self, jar_ids: Vec, account_id: AccountId) -> AggregatedTokenAmountView { - let mut detailed_amounts = HashMap::::new(); - let mut total_amount: TokenAmount = 0; + fn restake_all(&mut self, product_ids: Option>) { + let account_id = env::predecessor_account_id(); - for jar in self.account_jars_with_ids(&account_id, &jar_ids) { - let id = jar.id; - let principal = jar.principal; + self.migrate_account_if_needed(&account_id); - detailed_amounts.insert(U32(id), U128(principal)); - total_amount += principal; + let product_ids = product_ids.unwrap_or_else(|| { + self.get_account(&account_id) + .jars + .keys() + .filter(|product_id| self.get_product(product_id).is_enabled) + .collect() + }); + for product_id in product_ids.iter() { + self.restake_internal(&self.get_product(product_id)); } - AggregatedTokenAmountView { - detailed: detailed_amounts, - total: U128(total_amount), - } + // TODO: add event logging } - // TODO: add v2 support - fn get_total_interest(&self, account_id: AccountId) -> AggregatedInterestView { - self.get_interest( - self.account_jars(&account_id).iter().map(|a| U32(a.id)).collect(), - account_id, - ) - } + fn unlock_jars_for_account(&mut self, account_id: AccountId) { + self.assert_manager(); + self.migrate_account_if_needed(&account_id); - // TODO: add v2 support - fn get_interest(&self, jar_ids: Vec, account_id: AccountId) -> AggregatedInterestView { - let now = env::block_timestamp_ms(); + let account = self.get_account_mut(&account_id); + for (_, jar) in account.jars.iter_mut() { + jar.is_pending_withdraw = false; + } + } +} +impl AccountV2 { + fn get_total_interest(&self) -> AggregatedInterestView { let mut detailed_amounts = HashMap::::new(); let mut total_amount: TokenAmount = 0; - let score = self - .get_score(&account_id) - .map(AccountScore::claimable_score) - .unwrap_or_default(); + for (product_id, jar) in self.jars { + let product = self.get_product(&product_id); + let interest = product.terms.get_interest(self, &jar).0; - for jar in self.account_jars_with_ids(&account_id, &jar_ids) { - let product = self.get_product(&jar.product_id); - - let interest = jar.get_interest(&score, &product, now).0; - - detailed_amounts.insert(U32(jar.id), U128(interest)); + detailed_amounts.insert(product_id, interest.into()); total_amount += interest; } @@ -144,39 +125,32 @@ impl JarApi for Contract { detailed: detailed_amounts, total: U128(total_amount), }, - timestamp: now, - } - } - - fn restake(&mut self, product_id: ProductId) { - self.migrate_account_if_needed(&env::predecessor_account_id()); - self.restake_internal(&product_id); - - // TODO: add event logging + timestamp: env::block_timestamp_ms(), + }; } +} - fn restake_all(&mut self, product_ids: Option>) { - let account_id = env::predecessor_account_id(); - - self.migrate_account_if_needed(&account_id); - - let product_ids = product_ids.unwrap_or_else(|| self.get_account(&account_id).jars.keys().collect()); - for product_id in product_ids { - self.restake_internal(&product_id); +impl From<&AccountV1> for AccountV2 { + fn from(value: &AccountV1) -> Self { + let mut account = AccountV2 { + nonce: value.last_id, + jars: Default::default(), + score: value.score, + is_penalty_applied: false, + }; + + for jar in value.jars { + let deposit = Deposit::new(jar.created_at, jar.principal); + account.push(&jar.product_id, deposit); + + if !account.is_penalty_applied { + account.is_penalty_applied = jar.is_penalty_applied; + } } - // TODO: add event logging - } + // TODO: update and migrate cache + // TODO: migrate remainders - // TODO: add v2 support - fn unlock_jars_for_account(&mut self, account_id: AccountId) { - self.assert_manager(); - self.migrate_account_if_needed(&account_id); - - let jars = self.accounts.get_mut(&account_id).expect("Account doesn't have jars"); - - for jar in &mut jars.jars { - jar.is_pending_withdraw = false; - } + account } } diff --git a/contract/src/jar/model/common.rs b/contract/src/jar/model/common.rs index 301d2d61..ae82d118 100644 --- a/contract/src/jar/model/common.rs +++ b/contract/src/jar/model/common.rs @@ -1,12 +1,21 @@ +use std::cmp; + use near_sdk::{ env, json_types::{Base64VecU8, U128, U64}, near, AccountId, }; -use sweat_jar_model::{jar::JarId, Timezone, TokenAmount}; +use sweat_jar_model::{jar::JarId, Score, Timezone, TokenAmount, UDecimal, MS_IN_DAY, MS_IN_YEAR}; use crate::{ - common::Timestamp, jar::model::Jar, product::model::v2::Terms, score::AccountScore, Contract, JarsStorage, + common::Timestamp, + jar::model::{Jar, JarLastVersion}, + product::model::{ + v1::{Apy, Product}, + v2::{ProductV2, Terms}, + }, + score::AccountScore, + Contract, JarsStorage, }; /// The `JarTicket` struct represents a request to create a deposit jar for a corresponding product. @@ -69,67 +78,6 @@ impl Contract { account.deposit(product_id, amount); } - // pub(crate) fn create_jar( - // &mut self, - // account_id: AccountId, - // ticket: JarTicket, - // amount: U128, - // signature: Option, - // ) -> JarView { - // let amount = amount.0; - // let product_id = &ticket.product_id; - // let product = self.get_product(product_id); - // - // product.assert_enabled(); - // product.assert_cap(amount); - // - // if matches!(product.terms, Terms::ScoreBased(_)) { - // match (ticket.timezone, self.get_score_mut(&account_id)) { - // // Time zone already set. No actions required. - // (Some(_) | None, Some(_)) => (), - // (Some(timezone), None) => { - // self.accounts.entry(account_id.clone()).or_default().score = AccountScore::new(timezone); - // } - // (None, None) => { - // panic_str(&format!( - // "Trying to create step base jar for account: '{account_id}' without providing time zone" - // )); - // } - // } - // } - // - // self.verify(&account_id, amount, &ticket, signature); - // - // let id = self.increment_and_get_last_jar_id(); - // let now = env::block_timestamp_ms(); - // let jar = Jar::create(id, account_id.clone(), product_id.clone(), amount, now); - // - // self.add_new_jar(&account_id, jar.clone()); - // - // emit(EventKind::CreateJar(jar.clone().into())); - // - // jar.into() - // } - - // pub(crate) fn delete_jar(&mut self, account_id: &AccountId, jar_id: JarId) { - // let jars = self - // .accounts - // .get_mut(account_id) - // .unwrap_or_else(|| panic_str(&format!("Account '{account_id}' doesn't exist"))); - // - // require!( - // !jars.is_empty(), - // "Trying to delete a jar from account without any jars." - // ); - // - // let jar_position = jars - // .iter() - // .position(|j| j.id == jar_id) - // .unwrap_or_else(|| panic_str(&format!("Jar with id {jar_id} doesn't exist"))); - // - // jars.swap_remove(jar_position); - // } - pub(crate) fn get_score(&self, account: &AccountId) -> Option<&AccountScore> { self.accounts.get(account).and_then(|a| a.score()) } @@ -173,3 +121,88 @@ impl Contract { .clone() } } + +impl JarLastVersion { + pub(crate) fn get_interest(&self, score: &[Score], product: &ProductV2, now: Timestamp) -> (TokenAmount, u64) { + if product.is_score_product() { + self.get_score_interest(score, product, now) + } else { + self.get_interest_with_apy(self.get_apy(product), product, now) + } + } + + fn get_apy(&self, product: &ProductV2) -> UDecimal { + match product.apy.clone() { + Apy::Constant(apy) => apy, + Apy::Downgradable(apy) => { + if self.is_penalty_applied { + apy.fallback + } else { + apy.default + } + } + } + } + + fn get_interest_for_term(&self, cache: u128, apy: UDecimal, term: Timestamp) -> (TokenAmount, u64) { + let term_in_milliseconds: u128 = term.into(); + + let yearly_interest = apy * self.principal; + + let ms_in_year: u128 = MS_IN_YEAR.into(); + + let interest = term_in_milliseconds * yearly_interest; + + // This will never fail because `MS_IN_YEAR` is u64 + // and remainder from u64 cannot be bigger than u64 so it is safe to unwrap here. + let remainder: u64 = (interest % ms_in_year).try_into().unwrap(); + + let interest = interest / ms_in_year; + + let total_remainder = self.claim_remainder + remainder; + + ( + cache + interest + u128::from(total_remainder / MS_IN_YEAR), + total_remainder % MS_IN_YEAR, + ) + } + + fn get_interest_with_apy(&self, apy: UDecimal, product: &ProductV2, now: Timestamp) -> (TokenAmount, u64) { + let (base_date, cache_interest) = if let Some(cache) = &self.cache { + (cache.updated_at, cache.interest) + } else { + (self.created_at, 0) + }; + + let until_date = self.get_interest_until_date(product, now); + + let effective_term = if until_date > base_date { + until_date - base_date + } else { + return (cache_interest, 0); + }; + + self.get_interest_for_term(cache_interest, apy, effective_term) + } + + fn get_score_interest(&self, score: &[Score], product: &ProductV2, now: Timestamp) -> (TokenAmount, u64) { + let cache = self.cache.map(|c| c.interest).unwrap_or_default(); + + if let Terms::Fixed(end_term) = &product.terms { + if now > end_term.lockup_term { + return (cache, 0); + } + } + + let apy = product.apy_for_score(score); + self.get_interest_for_term(cache, apy, MS_IN_DAY) + } + + fn get_interest_until_date(&self, product: &ProductV2, now: Timestamp) -> Timestamp { + match product.terms.clone() { + Terms::Fixed(value) => cmp::min(now, self.created_at + value.lockup_term), + Terms::ScoreBased(value) => cmp::min(now, self.created_at + value.lockup_term), + Terms::Flexible(_) => now, + } + } +} diff --git a/contract/src/jar/model/v2.rs b/contract/src/jar/model/v2.rs index d5285ab9..0ba14308 100644 --- a/contract/src/jar/model/v2.rs +++ b/contract/src/jar/model/v2.rs @@ -81,6 +81,14 @@ impl JarV2 { self } + pub(crate) fn clean_up_deposits(&mut self, partition_index: usize) { + if partition_index == self.deposits.len() { + self.deposits.clear(); + } else { + self.deposits.drain(..partition_index); + } + } + pub(crate) fn apply(&mut self, companion: JarV2Companion) -> &mut Self { if let Some(claim_remainder) = companion.claim_remainder { self.claim_remainder = claim_remainder; diff --git a/contract/src/jar/view.rs b/contract/src/jar/view.rs index f7803594..4c1e03ac 100644 --- a/contract/src/jar/view.rs +++ b/contract/src/jar/view.rs @@ -1,19 +1,15 @@ use near_sdk::json_types::{U128, U64}; -use sweat_jar_model::{jar::JarView, U32}; +use sweat_jar_model::{jar::JarView, ProductId, U32}; -use crate::jar::model::Jar; +use crate::jar::model::{Jar, JarV2}; impl From for JarView { fn from(value: Jar) -> Self { Self { - id: U32(value.id), - account_id: value.account_id.clone(), + id: value.id.into(), product_id: value.product_id.clone(), created_at: U64(value.created_at), principal: U128(value.principal), - claimed_balance: U128(value.claimed_balance), - is_penalty_applied: value.is_penalty_applied, - is_pending_withdraw: value.is_pending_withdraw, } } } @@ -21,14 +17,29 @@ impl From for JarView { impl From<&Jar> for JarView { fn from(value: &Jar) -> Self { Self { - id: U32(value.id), - account_id: value.account_id.clone(), + id: value.id.into(), product_id: value.product_id.clone(), created_at: U64(value.created_at), principal: U128(value.principal), - claimed_balance: U128(value.claimed_balance), - is_penalty_applied: value.is_penalty_applied, - is_pending_withdraw: value.is_pending_withdraw, } } } + +pub struct DetailedJarV2(ProductId, JarV2); + +impl From<&DetailedJarV2> for Vec { + fn from(value: &DetailedJarV2) -> Self { + let product_id = value.0.clone(); + value + .1 + .deposits + .iter() + .map(|deposit| JarView { + product_id, + id: format!("{product_id}_{}", deposit.created_at), + created_at: deposit.created_at.into(), + principal: deposit.principal.into(), + }) + .collect() + } +} diff --git a/contract/src/product/model/v2.rs b/contract/src/product/model/v2.rs index a2ea2757..ec09bbf2 100644 --- a/contract/src/product/model/v2.rs +++ b/contract/src/product/model/v2.rs @@ -308,9 +308,17 @@ impl Apy { } impl Contract { + // UnorderedMap doesn't have cache and deserializes `Product` on each get + // This cached getter significantly reduces gas usage pub(crate) fn get_product(&self, product_id: &ProductId) -> ProductV2 { - self.products - .get(product_id) - .unwrap_or_else(|| env::panic_str(format!("Product {product_id} is not found").as_str())) + self.products_cache + .borrow_mut() + .entry(product_id.clone()) + .or_insert_with(|| { + self.products + .get(product_id) + .unwrap_or_else(|| env::panic_str(format!("Product {product_id} is not found").as_str())) + }) + .clone() } } diff --git a/contract/src/withdraw/api.rs b/contract/src/withdraw/api.rs index 876a44d6..fda33c83 100644 --- a/contract/src/withdraw/api.rs +++ b/contract/src/withdraw/api.rs @@ -62,11 +62,10 @@ impl WithdrawApi for Contract { assert_not_locked(jar); jar.lock(); - // TODO: add method for withdrawal on a single jar - self.update_cache(account); + self.update_jar_cache(account, &product_id); let product = self.get_product(&product_id); - let (amount, partition_index) = jar.get_liquid_balance(product.terms, env::block_timestamp_ms()); + let (amount, partition_index) = jar.get_liquid_balance(&product.terms, env::block_timestamp_ms()); let fee = product.calculate_fee(amount); let request = WithdrawalRequest { @@ -207,11 +206,7 @@ impl Contract { } fn clean_up(request: &WithdrawalRequest, account: &mut AccountV2, jar: &mut JarV2) { - if jar.deposits.len() == request.partition_index { - jar.deposits.clear(); - } else { - jar.deposits.drain(..request.partition_index); - } + jar.clean_up_deposits(request.partition_index); if jar.should_close() { account.jars.remove(&request.product_id); diff --git a/model/src/api.rs b/model/src/api.rs index 2105985a..eecbf592 100644 --- a/model/src/api.rs +++ b/model/src/api.rs @@ -44,17 +44,6 @@ pub trait ClaimApi { /// The `JarApi` trait defines methods for managing deposit jars and their associated data within the smart contract. #[make_integration_version] pub trait JarApi { - /// Retrieves information about a specific deposit jar by its index. - /// - /// # Arguments - /// - /// * `jar_id` - The ID of the deposit jar for which information is being retrieved. - /// - /// # Returns - /// - /// A `JarView` struct containing details about the specified deposit jar. - fn get_jar(&self, account_id: AccountId, jar_id: JarIdView) -> JarView; - /// Retrieves information about all deposit jars associated with a given account. /// /// # Arguments @@ -66,32 +55,6 @@ pub trait JarApi { /// A `Vec` containing details about all deposit jars belonging to the specified account. fn get_jars_for_account(&self, account_id: AccountId) -> Vec; - /// Retrieves the total principal amount across all deposit jars for a provided account. - /// - /// # Arguments - /// - /// * `account_id` - The `AccountId` of the account for which the total principal is being retrieved. - /// - /// # Returns - /// - /// An `U128` representing the sum of principal amounts across all deposit jars for the specified account. - /// Returns 0 if the account has no associated jars. - fn get_total_principal(&self, account_id: AccountId) -> AggregatedTokenAmountView; - - /// Retrieves the principal amount for a specific set of deposit jars. - /// - /// # Arguments - /// - /// * `jar_ids` - A `Vec` containing the IDs of the deposit jars for which the - /// principal is being retrieved. - /// - /// * `account_id` - The `AccountId` of the account for which the principal is being retrieved. - /// - /// # Returns - /// - /// An `U128` representing the sum of principal amounts for the specified deposit jars. - fn get_principal(&self, jar_ids: Vec, account_id: AccountId) -> AggregatedTokenAmountView; - /// Retrieves the total interest amount across all deposit jars for a provided account. /// /// # Arguments @@ -104,19 +67,6 @@ pub trait JarApi { /// Returns 0 if the account has no associated jars. fn get_total_interest(&self, account_id: AccountId) -> AggregatedInterestView; - /// Retrieves the interest amount for a specific set of deposit jars. - /// - /// # Arguments - /// - /// * `jar_ids` - A `Vec` containing the IDs of the deposit jars for which the - /// interest is being retrieved. - /// - /// # Returns - /// - /// An `U128` representing the sum of interest amounts for the specified deposit jars. - /// - fn get_interest(&self, jar_ids: Vec, account_id: AccountId) -> AggregatedInterestView; - /// Restakes the contents of a specified deposit jar into a new jar. /// /// # Arguments diff --git a/model/src/jar.rs b/model/src/jar.rs index eddf86c5..cd7af2fa 100644 --- a/model/src/jar.rs +++ b/model/src/jar.rs @@ -9,20 +9,15 @@ use crate::{numbers::U32, ProductId}; pub type JarId = u32; -pub type JarIdView = U32; +pub type JarIdView = String; #[derive(Clone, Debug, PartialEq)] #[near(serializers=[json])] pub struct JarView { pub id: JarIdView, - pub account_id: AccountId, pub product_id: ProductId, pub created_at: U64, pub principal: U128, - pub claimed_balance: U128, - pub is_penalty_applied: bool, - #[serde(default)] - pub is_pending_withdraw: bool, } #[derive(Debug, Clone, PartialEq)] @@ -41,7 +36,7 @@ impl Default for AggregatedTokenAmountView { } } -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Default)] #[near(serializers=[json])] pub struct AggregatedInterestView { pub amount: AggregatedTokenAmountView, From 03de827478faf96e92d546db4b72d0e8387c081b Mon Sep 17 00:00:00 2001 From: Vasily Styagov Date: Fri, 11 Oct 2024 20:12:19 +0100 Subject: [PATCH 08/93] some fixes --- contract/src/claim/api.rs | 2 - contract/src/ft_receiver.rs | 8 +-- contract/src/jar/account/v2.rs | 4 +- contract/src/jar/model/common.rs | 85 -------------------------------- contract/src/penalty/api.rs | 4 +- 5 files changed, 5 insertions(+), 98 deletions(-) diff --git a/contract/src/claim/api.rs b/contract/src/claim/api.rs index ceb9f35e..28751f39 100644 --- a/contract/src/claim/api.rs +++ b/contract/src/claim/api.rs @@ -79,7 +79,6 @@ impl Contract { account_rollback, // TODO: add events EventKind::Claim(vec![]), - now, ) } else { PromiseOrValue::Value(accumulator) @@ -95,7 +94,6 @@ impl Contract { claimed_amount: ClaimedAmountView, account_rollback: AccountV2Companion, event: EventKind, - now: Timestamp, ) -> PromiseOrValue { PromiseOrValue::Value(self.after_claim_internal( account_id.clone(), diff --git a/contract/src/ft_receiver.rs b/contract/src/ft_receiver.rs index 0c83099f..56d5201b 100644 --- a/contract/src/ft_receiver.rs +++ b/contract/src/ft_receiver.rs @@ -14,9 +14,6 @@ pub enum FtMessage { /// Represents a request to create `DeFi` Jars from provided `CeFi` Jars. Migrate(Vec), - - /// Represents a request to refill (top up) an existing jar using its `JarId`. - TopUp(JarId), } /// The `StakeMessage` struct represents a request to create a new jar for a corresponding product. @@ -42,16 +39,13 @@ impl FungibleTokenReceiver for Contract { match ft_message { FtMessage::Stake(message) => { let receiver_id = message.receiver_id.unwrap_or(sender_id); - self.create_jar(receiver_id, message.ticket, amount, message.signature); + self.deposit(receiver_id, message.ticket, amount, message.signature); } FtMessage::Migrate(jars) => { require!(sender_id == self.manager, "Migration can be performed only by admin"); self.migrate_jars(jars, amount); } - FtMessage::TopUp(jar_id) => { - self.top_up(&sender_id, jar_id, amount); - } } PromiseOrValue::Value(0.into()) diff --git a/contract/src/jar/account/v2.rs b/contract/src/jar/account/v2.rs index 5d4368da..38f17f00 100644 --- a/contract/src/jar/account/v2.rs +++ b/contract/src/jar/account/v2.rs @@ -53,7 +53,7 @@ impl Contract { } pub(crate) fn get_or_create_account_mut(&mut self, account_id: &AccountId) -> &mut AccountV2 { - if !self.accounts_v2.contains_key(&account_id) { + if !self.accounts_v2.contains_key(account_id) { self.accounts_v2.insert(account_id.clone(), AccountV2::default()); } @@ -64,7 +64,7 @@ impl Contract { impl AccountV2 { pub(crate) fn get_jar_mut(&mut self, product_id: &ProductId) -> &mut JarV2 { self.jars - .get_mut(&product_id) + .get_mut(product_id) .unwrap_or_else(|| env::panic_str(format!("Jar for product {product_id} is not found").as_str())) } diff --git a/contract/src/jar/model/common.rs b/contract/src/jar/model/common.rs index ae82d118..34d4bf29 100644 --- a/contract/src/jar/model/common.rs +++ b/contract/src/jar/model/common.rs @@ -121,88 +121,3 @@ impl Contract { .clone() } } - -impl JarLastVersion { - pub(crate) fn get_interest(&self, score: &[Score], product: &ProductV2, now: Timestamp) -> (TokenAmount, u64) { - if product.is_score_product() { - self.get_score_interest(score, product, now) - } else { - self.get_interest_with_apy(self.get_apy(product), product, now) - } - } - - fn get_apy(&self, product: &ProductV2) -> UDecimal { - match product.apy.clone() { - Apy::Constant(apy) => apy, - Apy::Downgradable(apy) => { - if self.is_penalty_applied { - apy.fallback - } else { - apy.default - } - } - } - } - - fn get_interest_for_term(&self, cache: u128, apy: UDecimal, term: Timestamp) -> (TokenAmount, u64) { - let term_in_milliseconds: u128 = term.into(); - - let yearly_interest = apy * self.principal; - - let ms_in_year: u128 = MS_IN_YEAR.into(); - - let interest = term_in_milliseconds * yearly_interest; - - // This will never fail because `MS_IN_YEAR` is u64 - // and remainder from u64 cannot be bigger than u64 so it is safe to unwrap here. - let remainder: u64 = (interest % ms_in_year).try_into().unwrap(); - - let interest = interest / ms_in_year; - - let total_remainder = self.claim_remainder + remainder; - - ( - cache + interest + u128::from(total_remainder / MS_IN_YEAR), - total_remainder % MS_IN_YEAR, - ) - } - - fn get_interest_with_apy(&self, apy: UDecimal, product: &ProductV2, now: Timestamp) -> (TokenAmount, u64) { - let (base_date, cache_interest) = if let Some(cache) = &self.cache { - (cache.updated_at, cache.interest) - } else { - (self.created_at, 0) - }; - - let until_date = self.get_interest_until_date(product, now); - - let effective_term = if until_date > base_date { - until_date - base_date - } else { - return (cache_interest, 0); - }; - - self.get_interest_for_term(cache_interest, apy, effective_term) - } - - fn get_score_interest(&self, score: &[Score], product: &ProductV2, now: Timestamp) -> (TokenAmount, u64) { - let cache = self.cache.map(|c| c.interest).unwrap_or_default(); - - if let Terms::Fixed(end_term) = &product.terms { - if now > end_term.lockup_term { - return (cache, 0); - } - } - - let apy = product.apy_for_score(score); - self.get_interest_for_term(cache, apy, MS_IN_DAY) - } - - fn get_interest_until_date(&self, product: &ProductV2, now: Timestamp) -> Timestamp { - match product.terms.clone() { - Terms::Fixed(value) => cmp::min(now, self.created_at + value.lockup_term), - Terms::ScoreBased(value) => cmp::min(now, self.created_at + value.lockup_term), - Terms::Flexible(_) => now, - } - } -} diff --git a/contract/src/penalty/api.rs b/contract/src/penalty/api.rs index 019b0cf9..3b4a607d 100644 --- a/contract/src/penalty/api.rs +++ b/contract/src/penalty/api.rs @@ -20,7 +20,7 @@ impl PenaltyApi for Contract { let account = self.get_account_mut(&account_id); account.is_penalty_applied = value; - self.update_cache(account); + self.update_account_cache(account); emit(ApplyPenalty(PenaltyData { account_id, @@ -37,7 +37,7 @@ impl PenaltyApi for Contract { let account = self.get_account_mut(account_id); account.is_penalty_applied = value; - self.update_cache(account); + self.update_account_cache(account); } emit(BatchApplyPenalty(BatchPenaltyData { From b10c42551ea854bb9c5dd6f80293c3ce16db1d01 Mon Sep 17 00:00:00 2001 From: Vasily Styagov Date: Fri, 11 Oct 2024 20:38:26 +0100 Subject: [PATCH 09/93] some fixes --- contract/src/assert.rs | 5 +-- contract/src/claim/api.rs | 3 +- contract/src/event.rs | 11 +---- contract/src/ft_receiver.rs | 2 +- contract/src/internal.rs | 12 ++---- contract/src/jar/account/v2.rs | 11 ++--- contract/src/jar/api.rs | 14 +++---- contract/src/jar/model/common.rs | 13 +----- contract/src/jar/model/v2.rs | 3 +- contract/src/jar/model/versioned.rs | 65 +---------------------------- contract/src/jar/view.rs | 2 +- contract/src/lib.rs | 3 +- contract/src/migration/step_jars.rs | 2 +- contract/src/penalty/api.rs | 5 +-- contract/src/product/api.rs | 2 +- contract/src/product/model/mod.rs | 2 + contract/src/product/model/v2.rs | 4 +- contract/src/product/view.rs | 9 ++-- model/src/api.rs | 7 +--- model/src/claimed_amount_view.rs | 5 +-- model/src/jar.rs | 2 +- 21 files changed, 40 insertions(+), 142 deletions(-) diff --git a/contract/src/assert.rs b/contract/src/assert.rs index ed740722..a9fafb61 100644 --- a/contract/src/assert.rs +++ b/contract/src/assert.rs @@ -1,10 +1,7 @@ use near_sdk::require; use sweat_jar_model::TokenAmount; -use crate::{ - common::Timestamp, - jar::model::{Jar, JarV2}, -}; +use crate::jar::model::{Jar, JarV2}; pub(crate) fn assert_not_locked(jar: &JarV2) { require!(!jar.is_pending_withdraw, "Another operation on this Jar is in progress"); diff --git a/contract/src/claim/api.rs b/contract/src/claim/api.rs index 28751f39..0dfb416d 100644 --- a/contract/src/claim/api.rs +++ b/contract/src/claim/api.rs @@ -4,7 +4,6 @@ use near_sdk::{env, ext_contract, json_types::U128, near_bindgen, AccountId, Pro use sweat_jar_model::{api::ClaimApi, claimed_amount_view::ClaimedAmountView, jar::AggregatedTokenAmountView}; use crate::{ - common::Timestamp, event::{emit, EventKind}, internal::is_promise_success, jar::{ @@ -12,7 +11,7 @@ use crate::{ model::{JarV2, JarV2Companion}, }, product::model::v2::InterestCalculator, - Contract, ContractExt, JarsStorage, + Contract, ContractExt, }; #[allow(dead_code)] // False positive since rust 1.78. It is used from `ext_contract` macro. diff --git a/contract/src/event.rs b/contract/src/event.rs index e81b9279..2a6454d8 100644 --- a/contract/src/event.rs +++ b/contract/src/event.rs @@ -8,7 +8,7 @@ use crate::{ common::Timestamp, env, jar::model::{Jar, JarCache}, - product::model::Product, + product::model::ProductV2, PACKAGE_NAME, VERSION, }; @@ -16,7 +16,7 @@ use crate::{ #[near(serializers=[json])] #[serde(tag = "event", content = "data", rename_all = "snake_case")] pub enum EventKind { - RegisterProduct(Product), + RegisterProduct(ProductV2), CreateJar(EventJar), Claim(Vec), Withdraw(WithdrawData), @@ -145,12 +145,6 @@ impl From for SweatJarEvent { } } -#[mutants::skip] -#[cfg(not(test))] -pub(crate) fn emit(event: EventKind) { - log!("{}", SweatJarEvent::from(event).to_json_event_string()); -} - #[mutants::skip] #[cfg(test)] pub(crate) fn emit(event: EventKind) { @@ -172,7 +166,6 @@ impl SweatJarEvent { #[cfg(test)] mod test { - use std::str::FromStr; use near_sdk::{json_types::U128, AccountId}; diff --git a/contract/src/ft_receiver.rs b/contract/src/ft_receiver.rs index 56d5201b..67df7bcb 100644 --- a/contract/src/ft_receiver.rs +++ b/contract/src/ft_receiver.rs @@ -1,6 +1,6 @@ use near_contract_standards::fungible_token::receiver::FungibleTokenReceiver; use near_sdk::{json_types::U128, near, require, serde_json, AccountId, PromiseOrValue}; -use sweat_jar_model::jar::{CeFiJar, JarId}; +use sweat_jar_model::jar::CeFiJar; use crate::{jar::model::JarTicket, near_bindgen, Base64VecU8, Contract, ContractExt}; diff --git a/contract/src/internal.rs b/contract/src/internal.rs index 969e9dd5..9ff6dea6 100644 --- a/contract/src/internal.rs +++ b/contract/src/internal.rs @@ -1,17 +1,11 @@ -use std::{collections::HashMap, fmt::Display}; +use std::fmt::Display; use near_sdk::require; -use sweat_jar_model::{ - jar::{JarId, JarIdView}, - ProductId, -}; +use sweat_jar_model::jar::JarId; use crate::{ env, - jar::{ - account::{v1::AccountV1, versioned::Account}, - model::Jar, - }, + jar::{account::versioned::Account, model::Jar}, product::model::v2::ProductV2, AccountId, Contract, }; diff --git a/contract/src/jar/account/v2.rs b/contract/src/jar/account/v2.rs index 38f17f00..34868012 100644 --- a/contract/src/jar/account/v2.rs +++ b/contract/src/jar/account/v2.rs @@ -1,16 +1,11 @@ -use std::{ - collections::HashMap, - fmt::format, - ops::{Deref, DerefMut}, -}; +use std::collections::HashMap; use near_sdk::{env, env::panic_str, near, AccountId}; -use sweat_jar_model::{jar::JarId, ProductId, Timezone, TokenAmount}; +use sweat_jar_model::{ProductId, Timezone, TokenAmount}; use crate::{ common::Timestamp, - jar::model::{AccountJarsLegacy, Deposit, Jar, JarCache, JarV2, JarV2Companion}, - migration::account_jars_non_versioned::AccountJarsNonVersioned, + jar::model::{Deposit, JarCache, JarV2, JarV2Companion}, product::model::v2::{InterestCalculator, ProductV2}, score::AccountScore, Contract, diff --git a/contract/src/jar/api.rs b/contract/src/jar/api.rs index 874e9604..bdd27333 100644 --- a/contract/src/jar/api.rs +++ b/contract/src/jar/api.rs @@ -1,22 +1,20 @@ use std::collections::HashMap; -use near_sdk::{env, env::panic_str, json_types::U128, near_bindgen, require, AccountId}; +use near_sdk::{env, json_types::U128, near_bindgen, require, AccountId}; use sweat_jar_model::{ api::JarApi, - jar::{AggregatedInterestView, AggregatedTokenAmountView, JarId, JarIdView, JarView}, - ProductId, TokenAmount, JAR_BATCH_SIZE, U32, + jar::{AggregatedInterestView, AggregatedTokenAmountView, JarIdView, JarView}, + ProductId, TokenAmount, }; use crate::{ - event::{emit, EventKind}, jar::{ account::{v1::AccountV1, v2::AccountV2}, - model::{Deposit, Jar, JarV2}, + model::Deposit, view::DetailedJarV2, }, - product::model::v2::{InterestCalculator, ProductV2}, - score::AccountScore, - Contract, ContractExt, JarsStorage, + product::model::v2::ProductV2, + Contract, ContractExt, }; impl Contract { diff --git a/contract/src/jar/model/common.rs b/contract/src/jar/model/common.rs index 34d4bf29..601dafbf 100644 --- a/contract/src/jar/model/common.rs +++ b/contract/src/jar/model/common.rs @@ -1,21 +1,12 @@ -use std::cmp; - use near_sdk::{ env, json_types::{Base64VecU8, U128, U64}, near, AccountId, }; -use sweat_jar_model::{jar::JarId, Score, Timezone, TokenAmount, UDecimal, MS_IN_DAY, MS_IN_YEAR}; +use sweat_jar_model::{jar::JarId, Timezone, TokenAmount}; use crate::{ - common::Timestamp, - jar::model::{Jar, JarLastVersion}, - product::model::{ - v1::{Apy, Product}, - v2::{ProductV2, Terms}, - }, - score::AccountScore, - Contract, JarsStorage, + common::Timestamp, jar::model::Jar, product::model::v2::Terms, score::AccountScore, Contract, JarsStorage, }; /// The `JarTicket` struct represents a request to create a deposit jar for a corresponding product. diff --git a/contract/src/jar/model/v2.rs b/contract/src/jar/model/v2.rs index 0ba14308..e1614b57 100644 --- a/contract/src/jar/model/v2.rs +++ b/contract/src/jar/model/v2.rs @@ -1,5 +1,5 @@ use near_sdk::near; -use sweat_jar_model::{TokenAmount, UDecimal}; +use sweat_jar_model::TokenAmount; use crate::{ common::{Duration, Timestamp}, @@ -43,6 +43,7 @@ impl JarV2 { (sum, partition_index) } else { + // TODO: add argument to `is_liquid` let partition_index = self.deposits.partition_point(|deposit| deposit.is_liquid(now, todo!())); let sum = self.deposits[..partition_index] diff --git a/contract/src/jar/model/versioned.rs b/contract/src/jar/model/versioned.rs index b90677a3..d49e7dbe 100644 --- a/contract/src/jar/model/versioned.rs +++ b/contract/src/jar/model/versioned.rs @@ -6,15 +6,9 @@ use near_sdk::{ BorshDeserialize, BorshSerialize, }, serde::{Deserialize, Serialize}, - AccountId, }; -use sweat_jar_model::{jar::JarId, ProductId, Score, TokenAmount}; -use crate::{ - common::Timestamp, - jar::model::{v1::JarV1, JarCache, JarLastVersion}, - product::model::Product, -}; +use crate::jar::model::{v1::JarV1, JarLastVersion}; pub type Jar = JarVersioned; @@ -41,63 +35,6 @@ impl BorshDeserialize for JarVersioned { } } -impl JarVersioned { - pub fn create( - id: JarId, - account_id: AccountId, - product_id: ProductId, - principal: TokenAmount, - created_at: Timestamp, - ) -> Self { - JarLastVersion { - id, - account_id, - product_id, - principal, - created_at, - cache: None, - claimed_balance: 0, - is_pending_withdraw: false, - is_penalty_applied: false, - claim_remainder: 0, - } - .into() - } - - pub fn locked(&self) -> Self { - JarLastVersion { - is_pending_withdraw: true, - ..self.deref().clone() - } - .into() - } - - pub fn unlocked(&self) -> Self { - JarLastVersion { - is_pending_withdraw: false, - ..self.deref().clone() - } - .into() - } - - pub fn with_id(mut self, id: JarId) -> Self { - self.id = id; - self - } - - pub fn withdrawn(&self, score: &[Score], product: &Product, withdrawn_amount: TokenAmount, now: Timestamp) -> Self { - JarV1 { - principal: self.principal - withdrawn_amount, - cache: Some(JarCache { - updated_at: now, - interest: self.get_interest(score, product, now).0, - }), - ..self.deref().clone() - } - .into() - } -} - impl Deref for JarVersioned { type Target = JarLastVersion; fn deref(&self) -> &Self::Target { diff --git a/contract/src/jar/view.rs b/contract/src/jar/view.rs index 4c1e03ac..4ea1bca3 100644 --- a/contract/src/jar/view.rs +++ b/contract/src/jar/view.rs @@ -25,7 +25,7 @@ impl From<&Jar> for JarView { } } -pub struct DetailedJarV2(ProductId, JarV2); +pub(crate) struct DetailedJarV2(pub(crate) ProductId, pub(crate) JarV2); impl From<&DetailedJarV2> for Vec { fn from(value: &DetailedJarV2) -> Self { diff --git a/contract/src/lib.rs b/contract/src/lib.rs index 809bcff0..b9bad0fe 100644 --- a/contract/src/lib.rs +++ b/contract/src/lib.rs @@ -5,7 +5,6 @@ use near_sdk::{ BorshStorageKey, PanicOnDefault, }; use near_self_update_proc::SelfUpdate; -use product::model::{Apy, Product}; use sweat_jar_model::{api::InitApi, jar::JarId, ProductId}; use crate::{ @@ -85,6 +84,7 @@ pub(crate) enum StorageKey { /// Previous implementation of Score storage used on testnet. Is not used anymore. AccountScore, AccountsVersioned, + AccountsV2, } #[near_bindgen] @@ -102,6 +102,7 @@ impl InitApi for Contract { last_jar_id: 0, accounts: LookupMap::new(StorageKey::AccountsVersioned), products_cache: HashMap::default().into(), + accounts_v2: LookupMap::new(StorageKey::AccountsV2), } } } diff --git a/contract/src/migration/step_jars.rs b/contract/src/migration/step_jars.rs index d71da80f..1b362135 100644 --- a/contract/src/migration/step_jars.rs +++ b/contract/src/migration/step_jars.rs @@ -6,7 +6,7 @@ use sweat_jar_model::{api::MigrationToStepJars, jar::JarId, ProductId}; use crate::{ jar::model::AccountJarsLegacy, migration::account_jars_non_versioned::AccountJarsNonVersioned, - product::model::{Apy, Cap, Product, Terms, WithdrawalFee}, + product::model::v1::{Apy, Cap, Product, Terms, WithdrawalFee}, Contract, ContractExt, StorageKey, }; diff --git a/contract/src/penalty/api.rs b/contract/src/penalty/api.rs index 3b4a607d..82aee5da 100644 --- a/contract/src/penalty/api.rs +++ b/contract/src/penalty/api.rs @@ -1,5 +1,5 @@ use near_sdk::{env, near_bindgen, AccountId}; -use sweat_jar_model::{api::PenaltyApi, jar::JarIdView}; +use sweat_jar_model::api::PenaltyApi; use crate::{ event::{ @@ -7,8 +7,7 @@ use crate::{ EventKind::{ApplyPenalty, BatchApplyPenalty}, PenaltyData, }, - product::model::Apy, - Contract, ContractExt, JarsStorage, + Contract, ContractExt, }; #[near_bindgen] diff --git a/contract/src/product/api.rs b/contract/src/product/api.rs index 042b69c6..53a6e84f 100644 --- a/contract/src/product/api.rs +++ b/contract/src/product/api.rs @@ -1,4 +1,4 @@ -use near_sdk::{assert_one_yocto, env::panic_str, near_bindgen, require}; +use near_sdk::{assert_one_yocto, near_bindgen, require}; use sweat_jar_model::{ api::ProductApi, product::{ProductView, RegisterProductCommand}, diff --git a/contract/src/product/model/mod.rs b/contract/src/product/model/mod.rs index 4bafae0a..53c2ecab 100644 --- a/contract/src/product/model/mod.rs +++ b/contract/src/product/model/mod.rs @@ -1,2 +1,4 @@ pub(crate) mod v1; pub(crate) mod v2; + +pub use v2::ProductV2; diff --git a/contract/src/product/model/v2.rs b/contract/src/product/model/v2.rs index ec09bbf2..0fd7d7a7 100644 --- a/contract/src/product/model/v2.rs +++ b/contract/src/product/model/v2.rs @@ -1,4 +1,4 @@ -use std::{cmp, iter::Sum}; +use std::cmp; use near_contract_standards::non_fungible_token::Token; use near_sdk::{near, require}; @@ -8,7 +8,7 @@ use crate::{ common::{Duration, Timestamp}, env, jar::{ - account::{v2::AccountV2, versioned::Account}, + account::v2::AccountV2, model::{Deposit, JarV2}, }, score::AccountScore, diff --git a/contract/src/product/view.rs b/contract/src/product/view.rs index ae923e3e..e9dc32cf 100644 --- a/contract/src/product/view.rs +++ b/contract/src/product/view.rs @@ -3,13 +3,10 @@ use sweat_jar_model::product::{ ApyView, CapView, DowngradableApyView, FixedProductTermsView, ProductView, TermsView, WithdrawalFeeView, }; -use crate::{ - product::model::{Cap, DowngradableApy, Terms, WithdrawalFee}, - Apy, Product, -}; +use crate::product::model::ProductV2; -impl From for ProductView { - fn from(value: Product) -> Self { +impl From for ProductView { + fn from(value: ProductV2) -> Self { Self { id: value.id, apy: value.apy.into(), diff --git a/model/src/api.rs b/model/src/api.rs index eecbf592..4f2e87fc 100644 --- a/model/src/api.rs +++ b/model/src/api.rs @@ -1,14 +1,11 @@ -use near_sdk::{ - json_types::{Base64VecU8, U128}, - AccountId, -}; +use near_sdk::{json_types::Base64VecU8, AccountId}; #[cfg(feature = "integration-api")] use nitka::near_sdk; use nitka_proc::make_integration_version; use crate::{ claimed_amount_view::ClaimedAmountView, - jar::{AggregatedInterestView, AggregatedTokenAmountView, JarIdView, JarView}, + jar::{AggregatedInterestView, JarView}, product::{ProductView, RegisterProductCommand}, withdraw::{BulkWithdrawView, WithdrawView}, ProductId, Score, UTC, diff --git a/model/src/claimed_amount_view.rs b/model/src/claimed_amount_view.rs index 64447e20..de6a6232 100644 --- a/model/src/claimed_amount_view.rs +++ b/model/src/claimed_amount_view.rs @@ -1,9 +1,6 @@ use near_sdk::{json_types::U128, near}; -use crate::{ - jar::{AggregatedTokenAmountView, JarId}, - ProductId, TokenAmount, U32, -}; +use crate::{jar::AggregatedTokenAmountView, ProductId, TokenAmount}; #[derive(Debug, PartialEq, Clone)] #[near(serializers=[json])] diff --git a/model/src/jar.rs b/model/src/jar.rs index cd7af2fa..d306ce74 100644 --- a/model/src/jar.rs +++ b/model/src/jar.rs @@ -5,7 +5,7 @@ use near_sdk::{ near, AccountId, Timestamp, }; -use crate::{numbers::U32, ProductId}; +use crate::ProductId; pub type JarId = u32; From ec5b34da6d7d3a4104c643fa38e0d516a71e852f Mon Sep 17 00:00:00 2001 From: Vasily Styagov Date: Fri, 11 Oct 2024 20:53:51 +0100 Subject: [PATCH 10/93] some fixes --- contract/src/product/model/v1.rs | 76 ++--------------------------- contract/src/score/account_score.rs | 1 - contract/src/test_utils.rs | 5 +- 3 files changed, 4 insertions(+), 78 deletions(-) diff --git a/contract/src/product/model/v1.rs b/contract/src/product/model/v1.rs index 0cac83c1..003734e3 100644 --- a/contract/src/product/model/v1.rs +++ b/contract/src/product/model/v1.rs @@ -1,7 +1,7 @@ -use near_sdk::{near, require}; -use sweat_jar_model::{ProductId, Score, ToAPY, TokenAmount, UDecimal}; +use near_sdk::near; +use sweat_jar_model::{ProductId, Score, TokenAmount, UDecimal}; -use crate::{common::Duration, env}; +use crate::common::Duration; /// The `Product` struct describes the terms of a deposit jar. It can be of Flexible or Fixed type. #[near(serializers=[borsh, json])] @@ -104,73 +104,3 @@ pub struct Cap { /// The maximum amount of tokens that can be stored in the jar. pub max: TokenAmount, } - -impl Product { - pub(crate) fn is_score_product(&self) -> bool { - self.score_cap > 0 - } - - pub(crate) fn apy_for_score(&self, score: &[Score]) -> UDecimal { - let total_score: Score = score.iter().map(|score| score.min(&self.score_cap)).sum(); - total_score.to_apy() - } - - pub(crate) fn is_flexible(&self) -> bool { - self.terms == Terms::Flexible - } - - pub(crate) fn allows_top_up(&self) -> bool { - self.is_enabled - && match &self.terms { - Terms::Fixed(value) => value.allows_top_up, - Terms::Flexible => true, - } - } - - pub(crate) fn allows_restaking(&self) -> bool { - match &self.terms { - Terms::Fixed(value) => value.allows_restaking, - Terms::Flexible => false, - } - } - - pub(crate) fn assert_cap(&self, amount: TokenAmount) { - if self.cap.min > amount || amount > self.cap.max { - env::panic_str(&format!( - "Total amount is out of product bounds: [{}..{}]", - self.cap.min, self.cap.max - )); - } - } - - pub(crate) fn assert_enabled(&self) { - require!(self.is_enabled, "It's not possible to create new jars for this product"); - } - - /// Check if fee in new product is not to high - pub(crate) fn assert_fee_amount(&self) { - let Some(ref fee) = self.withdrawal_fee else { - return; - }; - - let fee_ok = match fee { - WithdrawalFee::Fix(amount) => amount < &self.cap.min, - WithdrawalFee::Percent(percent) => percent.to_f32() < 100.0, - }; - - require!( - fee_ok, - "Fee for this product is too high. It is possible for customer to pay more in fees than he staked." - ); - } -} - -#[cfg(test)] -impl Product { - pub(crate) fn get_lockup_term(&self) -> Option { - match self.clone().terms { - Terms::Fixed(value) => Some(value.lockup_term), - Terms::Flexible => None, - } - } -} diff --git a/contract/src/score/account_score.rs b/contract/src/score/account_score.rs index dc676ad8..e321866c 100644 --- a/contract/src/score/account_score.rs +++ b/contract/src/score/account_score.rs @@ -135,7 +135,6 @@ mod test { use sweat_jar_model::{Day, Timezone, MS_IN_DAY, MS_IN_HOUR, UTC}; use crate::{ - product::model::Product, score::{account_score::Chain, AccountScore}, test_builder::TestBuilder, }; diff --git a/contract/src/test_utils.rs b/contract/src/test_utils.rs index bc0f433f..09049059 100644 --- a/contract/src/test_utils.rs +++ b/contract/src/test_utils.rs @@ -8,10 +8,7 @@ use sweat_jar_model::{TokenAmount, UDecimal}; use crate::{ common::Timestamp, jar::model::{Jar, JarLastVersion}, - product::{ - helpers::MessageSigner, - model::{Apy, DowngradableApy, Product}, - }, + product::helpers::MessageSigner, }; pub const PRINCIPAL: u128 = 1_000_000; From e733984d2a213ce1a06fc4ab1a3e1bd1be5ceb03 Mon Sep 17 00:00:00 2001 From: Vasily Styagov Date: Mon, 14 Oct 2024 13:34:25 +0100 Subject: [PATCH 11/93] fix mappers --- contract/src/event.rs | 6 + contract/src/internal.rs | 1 - contract/src/jar/account/v2.rs | 4 +- contract/src/jar/view.rs | 2 +- contract/src/product/api.rs | 4 +- contract/src/product/command.rs | 70 +++++++---- contract/src/product/model/v2.rs | 2 - contract/src/product/tests.rs | 20 +-- contract/src/product/view.rs | 20 +-- contract/src/score/tests.rs | 4 +- contract/src/withdraw/api.rs | 8 +- integration-tests/src/product.rs | 2 +- integration-tests/src/testnet/recovery.rs | 10 +- model/src/api.rs | 7 +- model/src/product.rs | 146 +++++++++++++--------- 15 files changed, 179 insertions(+), 127 deletions(-) diff --git a/contract/src/event.rs b/contract/src/event.rs index 2a6454d8..d6081f86 100644 --- a/contract/src/event.rs +++ b/contract/src/event.rs @@ -145,6 +145,12 @@ impl From for SweatJarEvent { } } +#[mutants::skip] +#[cfg(not(test))] +pub(crate) fn emit(event: EventKind) { + log!("{}", SweatJarEvent::from(event).to_json_event_string()); +} + #[mutants::skip] #[cfg(test)] pub(crate) fn emit(event: EventKind) { diff --git a/contract/src/internal.rs b/contract/src/internal.rs index 9ff6dea6..b2b5015d 100644 --- a/contract/src/internal.rs +++ b/contract/src/internal.rs @@ -6,7 +6,6 @@ use sweat_jar_model::jar::JarId; use crate::{ env, jar::{account::versioned::Account, model::Jar}, - product::model::v2::ProductV2, AccountId, Contract, }; diff --git a/contract/src/jar/account/v2.rs b/contract/src/jar/account/v2.rs index 34868012..06379220 100644 --- a/contract/src/jar/account/v2.rs +++ b/contract/src/jar/account/v2.rs @@ -88,9 +88,7 @@ impl AccountV2 { self.score = AccountScore::new(timezone); } (None, None) => { - panic_str(&format!( - "Trying to create step base jar for account: '{account_id}' without providing time zone" - )); + panic_str("Trying to create score based jar for without providing time zone"); } } } diff --git a/contract/src/jar/view.rs b/contract/src/jar/view.rs index 4ea1bca3..afc6e4ad 100644 --- a/contract/src/jar/view.rs +++ b/contract/src/jar/view.rs @@ -1,5 +1,5 @@ use near_sdk::json_types::{U128, U64}; -use sweat_jar_model::{jar::JarView, ProductId, U32}; +use sweat_jar_model::{jar::JarView, ProductId}; use crate::jar::model::{Jar, JarV2}; diff --git a/contract/src/product/api.rs b/contract/src/product/api.rs index 53a6e84f..dd7b5624 100644 --- a/contract/src/product/api.rs +++ b/contract/src/product/api.rs @@ -1,7 +1,7 @@ use near_sdk::{assert_one_yocto, near_bindgen, require}; use sweat_jar_model::{ api::ProductApi, - product::{ProductView, RegisterProductCommand}, + product::{ProductDto, ProductView}, ProductId, }; @@ -14,7 +14,7 @@ use crate::{ #[near_bindgen] impl ProductApi for Contract { #[payable] - fn register_product(&mut self, command: RegisterProductCommand) { + fn register_product(&mut self, command: ProductDto) { self.assert_manager(); assert_one_yocto(); diff --git a/contract/src/product/command.rs b/contract/src/product/command.rs index db1f8745..3ed53101 100644 --- a/contract/src/product/command.rs +++ b/contract/src/product/command.rs @@ -1,39 +1,28 @@ use sweat_jar_model::{ - product::{RegisterProductCommand, TermsDto, WithdrawalFeeDto}, + product::{ApyDto, ProductDto, TermsDto, WithdrawalFeeDto}, UDecimal, }; -use crate::product::model::{Apy, Cap, DowngradableApy, FixedProductTerms, Product, Terms, WithdrawalFee}; - -impl From for Product { - fn from(value: RegisterProductCommand) -> Self { - let apy = if let Some(apy_fallback) = value.apy_fallback { - Apy::Downgradable(DowngradableApy { - default: UDecimal::new(value.apy_default.0 .0, value.apy_default.1), - fallback: UDecimal::new(apy_fallback.0 .0, apy_fallback.1), - }) - } else { - Apy::Constant(UDecimal::new(value.apy_default.0 .0, value.apy_default.1)) - }; - let withdrawal_fee = value.withdrawal_fee.map(|dto| match dto { - WithdrawalFeeDto::Fix(value) => WithdrawalFee::Fix(value.0), - WithdrawalFeeDto::Percent(significand, exponent) => { - WithdrawalFee::Percent(UDecimal::new(significand.0, exponent)) - } - }); +use crate::product::model::{ + v2::{ + Apy, Cap, DowngradableApy, FixedProductTerms, FlexibleProductTerms, ScoreBasedProductTerms, Terms, + WithdrawalFee, + }, + ProductV2, +}; +impl From for ProductV2 { + fn from(value: ProductDto) -> Self { Self { id: value.id, - apy, cap: Cap { - min: value.cap_min.0, - max: value.cap_max.0, + min: value.cap.0 .0, + max: value.cap.1 .0, }, terms: value.terms.into(), - withdrawal_fee, + withdrawal_fee: value.withdrawal_fee.map(Into::into), public_key: value.public_key.map(|key| key.0), is_enabled: value.is_enabled, - score_cap: value.score_cap, } } } @@ -42,11 +31,38 @@ impl From for Terms { fn from(value: TermsDto) -> Self { match value { TermsDto::Fixed(value) => Terms::Fixed(FixedProductTerms { + apy: value.apy.into(), lockup_term: value.lockup_term.0, - allows_top_up: value.allows_top_up, - allows_restaking: value.allows_restaking, }), - TermsDto::Flexible => Terms::Flexible, + TermsDto::Flexible(value) => Terms::Flexible(FlexibleProductTerms { apy: value.apy.into() }), + TermsDto::ScoreBased(value) => Terms::ScoreBased(ScoreBasedProductTerms { + score_cap: value.score_cap, + base_apy: value.base_apy.into(), + lockup_term: value.lockup_term.0, + }), + } + } +} + +impl From for Apy { + fn from(value: ApyDto) -> Self { + match value.fallback { + None => Apy::Constant(value.default.into()), + Some(fallback) => Apy::Downgradable(DowngradableApy { + default: value.default.into(), + fallback: fallback.into(), + }), + } + } +} + +impl From for WithdrawalFee { + fn from(value: WithdrawalFeeDto) -> Self { + match value { + WithdrawalFeeDto::Fix(value) => WithdrawalFee::Fix(value.0), + WithdrawalFeeDto::Percent(significand, exponent) => { + WithdrawalFee::Percent(UDecimal::new(significand.0, exponent)) + } } } } diff --git a/contract/src/product/model/v2.rs b/contract/src/product/model/v2.rs index 0fd7d7a7..5abd8355 100644 --- a/contract/src/product/model/v2.rs +++ b/contract/src/product/model/v2.rs @@ -1,6 +1,5 @@ use std::cmp; -use near_contract_standards::non_fungible_token::Token; use near_sdk::{near, require}; use sweat_jar_model::{ProductId, Score, ToAPY, TokenAmount, UDecimal, MS_IN_YEAR}; @@ -11,7 +10,6 @@ use crate::{ account::v2::AccountV2, model::{Deposit, JarV2}, }, - score::AccountScore, Contract, }; diff --git a/contract/src/product/tests.rs b/contract/src/product/tests.rs index 2f2a7c33..dd5bd35a 100644 --- a/contract/src/product/tests.rs +++ b/contract/src/product/tests.rs @@ -7,7 +7,7 @@ use near_sdk::{ use sweat_jar_model::{ api::ProductApi, product::{ - ApyView, DowngradableApyView, FixedProductTermsDto, ProductView, RegisterProductCommand, TermsDto, TermsView, + ApyView, DowngradableApyView, FixedProductTermsDto, ProductDto, ProductView, TermsDto, TermsView, WithdrawalFeeDto, WithdrawalFeeView, }, UDecimal, MS_IN_YEAR, @@ -22,8 +22,8 @@ use crate::{ test_utils::admin, }; -pub(crate) fn get_register_product_command() -> RegisterProductCommand { - RegisterProductCommand { +pub(crate) fn get_register_product_command() -> ProductDto { + ProductDto { id: "product".to_string(), ..Default::default() } @@ -87,7 +87,7 @@ fn register_product_with_existing_id() { }); } -fn register_product(command: RegisterProductCommand) -> (Product, ProductView) { +fn register_product(command: ProductDto) -> (Product, ProductView) { let admin = admin(); let mut context = Context::new(admin.clone()); @@ -103,7 +103,7 @@ fn register_product(command: RegisterProductCommand) -> (Product, ProductView) { #[test] fn register_downgradable_product() { - let (product, view) = register_product(RegisterProductCommand { + let (product, view) = register_product(ProductDto { id: "downgradable_product".to_string(), apy_fallback: Some((U128(10), 3)), ..Default::default() @@ -137,7 +137,7 @@ fn register_downgradable_product() { expected = "Fee for this product is too high. It is possible for customer to pay more in fees than he staked." )] fn register_product_with_too_high_fixed_fee() { - register_product(RegisterProductCommand { + register_product(ProductDto { id: "product_with_fixed_fee".to_string(), withdrawal_fee: WithdrawalFeeDto::Fix(U128(200)).into(), terms: TermsDto::Fixed(FixedProductTermsDto { @@ -154,7 +154,7 @@ fn register_product_with_too_high_fixed_fee() { expected = "Fee for this product is too high. It is possible for customer to pay more in fees than he staked." )] fn register_product_with_too_high_percent_fee() { - register_product(RegisterProductCommand { + register_product(ProductDto { id: "product_with_fixed_fee".to_string(), withdrawal_fee: WithdrawalFeeDto::Percent(U128(100), 0).into(), ..Default::default() @@ -163,7 +163,7 @@ fn register_product_with_too_high_percent_fee() { #[test] fn register_product_with_fee() { - let (product, view) = register_product(RegisterProductCommand { + let (product, view) = register_product(ProductDto { id: "product_with_fixed_fee".to_string(), withdrawal_fee: WithdrawalFeeDto::Fix(U128(10)).into(), ..Default::default() @@ -173,7 +173,7 @@ fn register_product_with_fee() { assert_eq!(view.withdrawal_fee, Some(WithdrawalFeeView::Fix(U128(10)))); - let (product, view) = register_product(RegisterProductCommand { + let (product, view) = register_product(ProductDto { id: "product_with_percent_fee".to_string(), withdrawal_fee: WithdrawalFeeDto::Percent(U128(12), 2).into(), ..Default::default() @@ -192,7 +192,7 @@ fn register_product_with_fee() { #[test] fn register_product_with_flexible_terms() { - let (product, view) = register_product(RegisterProductCommand { + let (product, view) = register_product(ProductDto { id: "product_with_fixed_fee".to_string(), terms: TermsDto::Flexible, ..Default::default() diff --git a/contract/src/product/view.rs b/contract/src/product/view.rs index e9dc32cf..024ec4be 100644 --- a/contract/src/product/view.rs +++ b/contract/src/product/view.rs @@ -1,20 +1,22 @@ use near_sdk::json_types::{U128, U64}; use sweat_jar_model::product::{ - ApyView, CapView, DowngradableApyView, FixedProductTermsView, ProductView, TermsView, WithdrawalFeeView, + ApyView, CapView, DowngradableApyView, FixedProductTermsView, FlexibleProductTermsView, ProductView, + ScoreBasedProductTermsView, TermsView, WithdrawalFeeView, }; -use crate::product::model::ProductV2; +use crate::product::model::{ + v2::{Apy, Cap, DowngradableApy, Terms, WithdrawalFee}, + ProductV2, +}; impl From for ProductView { fn from(value: ProductV2) -> Self { Self { id: value.id, - apy: value.apy.into(), cap: value.cap.into(), terms: value.terms.into(), withdrawal_fee: value.withdrawal_fee.map(Into::into), is_enabled: value.is_enabled, - score_cap: value.score_cap, } } } @@ -23,11 +25,15 @@ impl From for TermsView { fn from(value: Terms) -> Self { match value { Terms::Fixed(value) => TermsView::Fixed(FixedProductTermsView { + apy: value.apy.into(), lockup_term: U64(value.lockup_term), - allows_top_up: value.allows_top_up, - allows_restaking: value.allows_restaking, }), - Terms::Flexible => TermsView::Flexible, + Terms::Flexible(value) => TermsView::Flexible(FlexibleProductTermsView { apy: value.apy.into() }), + Terms::ScoreBased(value) => TermsView::ScoreBased(ScoreBasedProductTermsView { + base_apy: value.base_apy.into(), + lockup_term: value.lockup_term.into(), + score_cap: value.score_cap, + }), } } } diff --git a/contract/src/score/tests.rs b/contract/src/score/tests.rs index b995e2d0..e8c65279 100644 --- a/contract/src/score/tests.rs +++ b/contract/src/score/tests.rs @@ -9,7 +9,7 @@ use near_sdk::{ use sweat_jar_model::{ api::{ProductApi, ScoreApi, WithdrawApi}, jar::JarId, - product::RegisterProductCommand, + product::ProductDto, Score, Timezone, MS_IN_DAY, UTC, }; @@ -34,7 +34,7 @@ fn record_score_by_non_manager() { fn create_invalid_step_product() { let mut ctx = TestBuilder::new().build(); - let mut command = RegisterProductCommand { + let mut command = ProductDto { id: "aa".to_string(), apy_default: (10.into(), 3), apy_fallback: None, diff --git a/contract/src/withdraw/api.rs b/contract/src/withdraw/api.rs index fda33c83..d7da68fa 100644 --- a/contract/src/withdraw/api.rs +++ b/contract/src/withdraw/api.rs @@ -1,4 +1,5 @@ -use common::test_data; +#[cfg(test)] +use common::test_data::get_test_future_success; use near_sdk::{ ext_contract, near_bindgen, serde::{Deserialize, Serialize}, @@ -268,7 +269,7 @@ impl Contract { account_id: &AccountId, request: WithdrawalRequest, ) -> PromiseOrValue { - let withdrawn = self.after_withdraw_internal(account_id.clone(), request, test_data::get_test_future_success()); + let withdrawn = self.after_withdraw_internal(account_id.clone(), request, get_test_future_success()); PromiseOrValue::Value(withdrawn) } @@ -278,8 +279,7 @@ impl Contract { account_id: &AccountId, request: BulkWithdrawalRequest, ) -> PromiseOrValue { - let withdrawn = - self.after_bulk_withdraw_internal(account_id.clone(), request, test_data::get_test_future_success()); + let withdrawn = self.after_bulk_withdraw_internal(account_id.clone(), request, get_test_future_success()); PromiseOrValue::Value(withdrawn) } diff --git a/integration-tests/src/product.rs b/integration-tests/src/product.rs index 6feb61ac..ea44e375 100644 --- a/integration-tests/src/product.rs +++ b/integration-tests/src/product.rs @@ -42,7 +42,7 @@ impl RegisterProductCommand { json } - pub(crate) fn get(self) -> sweat_jar_model::product::RegisterProductCommand { + pub(crate) fn get(self) -> sweat_jar_model::product::ProductDto { from_value(self.json()).unwrap() } diff --git a/integration-tests/src/testnet/recovery.rs b/integration-tests/src/testnet/recovery.rs index 3e82580f..e3feee29 100644 --- a/integration-tests/src/testnet/recovery.rs +++ b/integration-tests/src/testnet/recovery.rs @@ -9,19 +9,19 @@ use nitka::{ use sweat_jar_model::{ api::{ClaimApiIntegration, JarApiIntegration, ProductApiIntegration, SweatJarContract, WithdrawApiIntegration}, claimed_amount_view::ClaimedAmountView, - product::{FixedProductTermsDto, RegisterProductCommand, TermsDto, WithdrawalFeeDto}, + product::{FixedProductTermsDto, ProductDto, TermsDto, WithdrawalFeeDto}, MS_IN_DAY, MS_IN_SECOND, }; use tokio::time::sleep; use crate::{jar_contract_extensions::JarContractExtensions, testnet::testnet_context::TestnetContext}; -fn _get_products() -> Vec { +fn _get_products() -> Vec { let json_str = read_to_string("../products_testnet.json").unwrap(); let json: Value = serde_json::from_str(&json_str).unwrap(); - let mut products: Vec = vec![]; + let mut products: Vec = vec![]; for product_val in json.as_array().unwrap() { let id = product_val["product_id"].as_str().unwrap().to_string(); @@ -53,7 +53,7 @@ fn _get_products() -> Vec { let lockup_seconds = product_val["lockup_seconds"].as_u64().unwrap(); - products.push(RegisterProductCommand { + products.push(ProductDto { id, apy_default: (((apy * 1000.0) as u128).into(), 3), apy_fallback: None, @@ -75,7 +75,7 @@ fn _get_products() -> Vec { } async fn register_test_product(manager: &Account, jar: &SweatJarContract<'_>) -> Result<()> { - jar.register_product(RegisterProductCommand { + jar.register_product(ProductDto { id: "5_days_20000_steps".to_string(), apy_default: (0.into(), 0), apy_fallback: None, diff --git a/model/src/api.rs b/model/src/api.rs index 4f2e87fc..d3486ce9 100644 --- a/model/src/api.rs +++ b/model/src/api.rs @@ -1,12 +1,13 @@ +#[cfg(not(feature = "integration-api"))] use near_sdk::{json_types::Base64VecU8, AccountId}; #[cfg(feature = "integration-api")] -use nitka::near_sdk; +use nitka::near_sdk::{json_types::Base64VecU8, AccountId}; use nitka_proc::make_integration_version; use crate::{ claimed_amount_view::ClaimedAmountView, jar::{AggregatedInterestView, JarView}, - product::{ProductView, RegisterProductCommand}, + product::{ProductDto, ProductView}, withdraw::{BulkWithdrawView, WithdrawView}, ProductId, Score, UTC, }; @@ -149,7 +150,7 @@ pub trait ProductApi { /// # Panics /// /// This method will panic if a product with the same id already exists. - fn register_product(&mut self, command: RegisterProductCommand); + fn register_product(&mut self, command: ProductDto); #[deposit_one_yocto] /// Sets the enabled status of a specific product. diff --git a/model/src/product.rs b/model/src/product.rs index 077dc4db..71aedb6b 100644 --- a/model/src/product.rs +++ b/model/src/product.rs @@ -5,6 +5,16 @@ use near_sdk::{ use crate::{ProductId, Score, MS_IN_YEAR}; +#[derive(Clone, Debug, PartialEq)] +#[near(serializers=[json])] +pub struct ProductView { + pub id: ProductId, + pub cap: CapView, + pub terms: TermsView, + pub withdrawal_fee: Option, + pub is_enabled: bool, +} + #[derive(Clone, Debug, PartialEq)] #[near(serializers=[json])] pub struct DowngradableApyView { @@ -26,20 +36,34 @@ pub struct CapView { pub max: U128, } +#[derive(Clone, Debug, PartialEq)] +#[near(serializers=[json])] +#[serde(tag = "type", content = "data", rename_all = "snake_case")] +pub enum TermsView { + Fixed(FixedProductTermsView), + Flexible(FlexibleProductTermsView), + ScoreBased(ScoreBasedProductTermsView), +} + #[derive(Clone, Debug, PartialEq)] #[near(serializers=[json])] pub struct FixedProductTermsView { + pub apy: ApyView, pub lockup_term: U64, - pub allows_top_up: bool, - pub allows_restaking: bool, } #[derive(Clone, Debug, PartialEq)] #[near(serializers=[json])] -#[serde(tag = "type", content = "data", rename_all = "snake_case")] -pub enum TermsView { - Fixed(FixedProductTermsView), - Flexible, +pub struct FlexibleProductTermsView { + pub apy: ApyView, +} + +#[derive(Clone, Debug, PartialEq)] +#[near(serializers=[json])] +pub struct ScoreBasedProductTermsView { + pub base_apy: ApyView, + pub lockup_term: U64, + pub score_cap: Score, } #[derive(Clone, Debug, PartialEq)] @@ -50,43 +74,79 @@ pub enum WithdrawalFeeView { Percent(f32), } -#[derive(Clone, Debug, PartialEq)] -#[near(serializers=[json])] -pub struct ProductView { +#[near(serializers=[borsh, json])] +#[derive(PartialEq, Clone, Debug)] +// TODO: doc change +pub struct ProductDto { pub id: ProductId, - pub apy: ApyView, - pub cap: CapView, - pub terms: TermsView, - pub withdrawal_fee: Option, + pub cap: (U128, U128), + pub terms: TermsDto, + pub withdrawal_fee: Option, + pub public_key: Option, pub is_enabled: bool, - #[serde(default)] - pub score_cap: Score, +} + +#[near(serializers=[borsh, json])] +#[derive(PartialEq, Clone, Debug)] +pub struct ApyDto { + pub default: (U128, u32), + pub fallback: Option<(U128, u32)>, +} + +#[near(serializers=[borsh, json])] +#[derive(PartialEq, Clone, Debug)] +#[serde(tag = "type", content = "data", rename_all = "snake_case")] +pub enum TermsDto { + Fixed(FixedProductTermsDto), + Flexible(FlexibleProductTermsDto), + ScoreBased(ScoreBasedProductTermsDto), } #[near(serializers=[borsh, json])] #[derive(PartialEq, Clone, Debug)] pub struct FixedProductTermsDto { + pub apy: ApyDto, pub lockup_term: U64, - pub allows_top_up: bool, - pub allows_restaking: bool, } -impl Default for FixedProductTermsDto { +#[near(serializers=[borsh, json])] +#[derive(PartialEq, Clone, Debug)] +pub struct FlexibleProductTermsDto { + pub apy: ApyDto, + pub lockup_term: U64, +} + +#[near(serializers=[borsh, json])] +#[derive(PartialEq, Clone, Debug)] +pub struct ScoreBasedProductTermsDto { + pub base_apy: ApyDto, + pub lockup_term: U64, + pub score_cap: Score, +} + +impl Default for ProductDto { fn default() -> Self { Self { - lockup_term: U64(MS_IN_YEAR), - allows_restaking: false, - allows_top_up: false, + id: "default_product".to_string(), + cap: (U128(100), U128(100_000_000_000)), + terms: TermsDto::default(), + withdrawal_fee: None, + public_key: None, + is_enabled: true, } } } -#[near(serializers=[borsh, json])] -#[derive(PartialEq, Clone, Debug)] -#[serde(tag = "type", content = "data", rename_all = "snake_case")] -pub enum TermsDto { - Fixed(FixedProductTermsDto), - Flexible, +impl Default for FixedProductTermsDto { + fn default() -> Self { + Self { + lockup_term: U64(MS_IN_YEAR), + apy: ApyDto { + default: (U128(12), 2), + fallback: None, + }, + } + } } impl Default for TermsDto { @@ -107,35 +167,3 @@ pub enum WithdrawalFeeDto { /// I.e. "0.12" becomes ("12", 2): 12 * 10^-2 Percent(U128, u32), } - -#[near(serializers=[borsh, json])] -#[derive(PartialEq, Clone, Debug)] -pub struct RegisterProductCommand { - pub id: ProductId, - pub apy_default: (U128, u32), - pub apy_fallback: Option<(U128, u32)>, - pub cap_min: U128, - pub cap_max: U128, - pub terms: TermsDto, - pub withdrawal_fee: Option, - pub public_key: Option, - pub is_enabled: bool, - pub score_cap: Score, -} - -impl Default for RegisterProductCommand { - fn default() -> Self { - Self { - id: "default_product".to_string(), - apy_default: (U128(12), 2), - apy_fallback: None, - cap_min: U128(100), - cap_max: U128(100_000_000_000), - terms: TermsDto::default(), - withdrawal_fee: None, - public_key: None, - is_enabled: true, - score_cap: 0, - } - } -} From 7aef67cef1eeb14fa403eae32be4ff3198a644a4 Mon Sep 17 00:00:00 2001 From: Vasily Styagov Date: Mon, 14 Oct 2024 17:45:22 +0100 Subject: [PATCH 12/93] fix withdraw api errors --- contract/src/claim/tests.rs | 436 ++++---- contract/src/common/tests.rs | 4 +- contract/src/ft_receiver.rs | 710 ++++++------- contract/src/jar/account/v2.rs | 12 +- contract/src/jar/model/v2.rs | 6 + contract/src/jar/tests/restake.rs | 308 +++--- contract/src/jar/tests/restake_all.rs | 308 +++--- contract/src/jar/tests/tests.rs | 484 ++++----- contract/src/product/helpers.rs | 80 +- contract/src/product/tests.rs | 564 +++++----- contract/src/score/account_score.rs | 174 ++-- contract/src/score/charts.rs | 340 +++--- contract/src/score/tests.rs | 622 +++++------ contract/src/test_builder/product_builder.rs | 74 +- contract/src/test_builder/test_access.rs | 132 +-- contract/src/test_builder/test_builder.rs | 134 +-- contract/src/test_utils.rs | 23 +- contract/src/tests.rs | 978 +++++++++--------- contract/src/withdraw/api.rs | 103 +- contract/src/withdraw/tests.rs | 814 +++++++-------- integration-tests/src/claim_detailed.rs | 128 +-- integration-tests/src/happy_flow.rs | 128 +-- integration-tests/src/jar_deletion.rs | 126 +-- integration-tests/src/many_jars.rs | 318 +++--- .../src/measure/after_withdraw.rs | 150 +-- .../src/measure/batch_penalty.rs | 188 ++-- integration-tests/src/measure/restake.rs | 186 ++-- integration-tests/src/measure/withdraw.rs | 198 ++-- integration-tests/src/measure/withdraw_all.rs | 86 +- integration-tests/src/migrations/defi.rs | 262 ++--- .../src/migrations/score_jars.rs | 262 ++--- integration-tests/src/premium_product.rs | 192 ++-- integration-tests/src/restake.rs | 276 ++--- integration-tests/src/testnet/recovery.rs | 412 ++++---- integration-tests/src/withdraw_all.rs | 210 ++-- integration-tests/src/withdraw_fee.rs | 204 ++-- 36 files changed, 4802 insertions(+), 4830 deletions(-) diff --git a/contract/src/claim/tests.rs b/contract/src/claim/tests.rs index 0f58f662..90b26a7e 100644 --- a/contract/src/claim/tests.rs +++ b/contract/src/claim/tests.rs @@ -1,218 +1,218 @@ -#![cfg(test)] - -use near_sdk::{json_types::U128, test_utils::test_env::alice, PromiseOrValue}; -use sweat_jar_model::{ - api::{ClaimApi, WithdrawApi}, - claimed_amount_view::ClaimedAmountView, - UDecimal, U32, -}; - -use crate::{ - common::{test_data::set_test_future_success, tests::Context}, - jar::model::Jar, - product::model::{Apy, Product}, - test_utils::{admin, UnwrapPromise}, -}; - -#[test] -fn claim_total_when_nothing_to_claim() { - let alice = alice(); - let admin = admin(); - - let product = Product::new(); - let jar = Jar::new(0).principal(100_000_000); - let mut context = Context::new(admin).with_products(&[product]).with_jars(&[jar]); - - context.switch_account(alice); - let value = context.contract().claim_total(None).unwrap(); - - assert_eq!(0, value.get_total().0); -} - -#[test] -fn claim_total_detailed_when_having_tokens() { - let alice = alice(); - let admin = admin(); - - let product = Product::new(); - let jar_0 = Jar::new(0).principal(100_000_000); - let jar_1 = Jar::new(1).principal(200_000_000); - let mut context = Context::new(admin) - .with_products(&[product.clone()]) - .with_jars(&[jar_0.clone(), jar_1.clone()]); - - let product_term = product.get_lockup_term().unwrap(); - let test_duration = product_term + 100; - - let jar_0_expected_interest = jar_0.get_interest(&[], &product, test_duration).0; - let jar_1_expected_interest = jar_1.get_interest(&[], &product, test_duration).0; - - context.set_block_timestamp_in_ms(test_duration); - - context.switch_account(&alice); - let result = context.contract().claim_total(Some(true)); - - let PromiseOrValue::Value(ClaimedAmountView::Detailed(value)) = result else { - panic!(); - }; - - assert_eq!(jar_0_expected_interest + jar_1_expected_interest, value.total.0); - - assert_eq!(jar_0_expected_interest, value.detailed.get(&U32(jar_0.id)).unwrap().0); - assert_eq!(jar_1_expected_interest, value.detailed.get(&U32(jar_1.id)).unwrap().0); -} - -#[test] -fn claim_pending_withdraw_jar() { - let alice = alice(); - let admin = admin(); - - let product = Product::new(); - let jar_0 = Jar::new(0).principal(100_000_000); - let jar_1 = Jar::new(1).principal(200_000_000).pending_withdraw(); - - let mut context = Context::new(admin) - .with_products(&[product.clone()]) - .with_jars(&[jar_0.clone(), jar_1.clone()]); - - let product_term = product.get_lockup_term().unwrap(); - let test_duration = product_term + 100; - - let jar_0_expected_interest = jar_0.get_interest(&[], &product, test_duration); - - context.set_block_timestamp_in_ms(test_duration); - - context.switch_account(&alice); - let result = context.contract().claim_total(Some(true)); - - let PromiseOrValue::Value(ClaimedAmountView::Detailed(value)) = result else { - panic!(); - }; - - assert_eq!(jar_0_expected_interest.0, value.total.0); - - assert_eq!(jar_0_expected_interest.0, value.detailed.get(&U32(jar_0.id)).unwrap().0); - assert_eq!(None, value.detailed.get(&U32(jar_1.id))); -} - -#[test] -fn dont_delete_jar_on_all_interest_claim() { - let alice = alice(); - let admin = admin(); - - let product = Product::new().apy(Apy::Constant(UDecimal::new(2, 1))); - let jar = Jar::new(0); - let mut context = Context::new(admin).with_products(&[product]).with_jars(&[jar.clone()]); - - context.set_block_timestamp_in_days(365); - - context.switch_account(&alice); - context.contract().claim_total(None); - - let jar = context.contract().get_jar_internal(&alice, jar.id); - assert_eq!(200_000, jar.claimed_balance); - - let Some(ref cache) = jar.cache else { panic!() }; - - assert_eq!(cache.interest, 0); - assert_eq!(jar.principal, 1_000_000); -} - -#[test] -#[should_panic(expected = "Jar with id: 0 doesn't exist")] -fn claim_all_withdraw_all_and_delete_jar() { - let alice = alice(); - let admin = admin(); - - let product = Product::new().apy(Apy::Constant(UDecimal::new(2, 1))); - let jar = Jar::new(0); - - let jar_id = jar.id; - - let mut context = Context::new(admin) - .with_products(&[product.clone()]) - .with_jars(&[jar.clone()]); - - context.set_block_timestamp_in_ms(product.get_lockup_term().unwrap() + 1); - - context.switch_account(&alice); - let claimed = context.contract().claim_total(None).unwrap(); - - assert_eq!(200_000, claimed.get_total().0); - - let jar = context.contract().get_jar_internal(&alice, jar_id); - assert_eq!(200_000, jar.claimed_balance); - - let Some(ref cache) = jar.cache else { panic!() }; - - assert_eq!(cache.interest, 0); - assert_eq!(jar.principal, 1_000_000); - - let withdrawn = context.contract().withdraw(U32(jar_id), None).unwrap(); - - assert_eq!(withdrawn.withdrawn_amount, U128(1_000_000)); - assert_eq!(withdrawn.fee, U128(0)); - - let _jar = context.contract().get_jar_internal(&alice, jar_id); -} - -#[test] -#[should_panic(expected = "Jar with id: 0 doesn't exist")] -fn withdraw_all_claim_all_and_delete_jar() { - let alice = alice(); - let admin = admin(); - - let product = Product::new().apy(Apy::Constant(UDecimal::new(2, 1))); - let jar = Jar::new(0); - - let jar_id = jar.id; - - let mut context = Context::new(admin) - .with_products(&[product.clone()]) - .with_jars(&[jar.clone()]); - - context.set_block_timestamp_in_ms(product.get_lockup_term().unwrap() + 1); - - context.switch_account(&alice); - - let withdrawn = context.contract().withdraw(U32(jar_id), None).unwrap(); - - assert_eq!(withdrawn.withdrawn_amount, U128(1_000_000)); - assert_eq!(withdrawn.fee, U128(0)); - - let jar = context.contract().get_jar_internal(&alice, jar_id); - - assert_eq!(jar.principal, 0); - - let claimed = context.contract().claim_total(None).unwrap(); - - assert_eq!(claimed.get_total(), U128(200_000)); - - let _jar = context.contract().get_jar_internal(&alice, jar_id); -} - -#[test] -fn failed_future_claim() { - set_test_future_success(false); - - let alice = alice(); - let admin = admin(); - - let product = Product::new().apy(Apy::Constant(UDecimal::new(2, 1))); - let jar = Jar::new(0); - let mut context = Context::new(admin).with_products(&[product]).with_jars(&[jar.clone()]); - - context.set_block_timestamp_in_days(365); - - context.switch_account(&alice); - - let jar_before_claim = context.contract().get_jar_internal(&alice, jar.id).clone(); - - let claimed = context.contract().claim_total(None).unwrap(); - - assert_eq!(claimed.get_total().0, 0); - - let jar_after_claim = context.contract().get_jar_internal(&alice, jar.id); - - assert_eq!(jar_before_claim, jar_after_claim); -} +// #![cfg(test)] +// +// use near_sdk::{json_types::U128, test_utils::test_env::alice, PromiseOrValue}; +// use sweat_jar_model::{ +// api::{ClaimApi, WithdrawApi}, +// claimed_amount_view::ClaimedAmountView, +// UDecimal, U32, +// }; +// +// use crate::{ +// common::{test_data::set_test_future_success, tests::Context}, +// jar::model::Jar, +// product::model::{Apy, Product}, +// test_utils::{admin, UnwrapPromise}, +// }; +// +// #[test] +// fn claim_total_when_nothing_to_claim() { +// let alice = alice(); +// let admin = admin(); +// +// let product = Product::new(); +// let jar = Jar::new(0).principal(100_000_000); +// let mut context = Context::new(admin).with_products(&[product]).with_jars(&[jar]); +// +// context.switch_account(alice); +// let value = context.contract().claim_total(None).unwrap(); +// +// assert_eq!(0, value.get_total().0); +// } +// +// #[test] +// fn claim_total_detailed_when_having_tokens() { +// let alice = alice(); +// let admin = admin(); +// +// let product = Product::new(); +// let jar_0 = Jar::new(0).principal(100_000_000); +// let jar_1 = Jar::new(1).principal(200_000_000); +// let mut context = Context::new(admin) +// .with_products(&[product.clone()]) +// .with_jars(&[jar_0.clone(), jar_1.clone()]); +// +// let product_term = product.get_lockup_term().unwrap(); +// let test_duration = product_term + 100; +// +// let jar_0_expected_interest = jar_0.get_interest(&[], &product, test_duration).0; +// let jar_1_expected_interest = jar_1.get_interest(&[], &product, test_duration).0; +// +// context.set_block_timestamp_in_ms(test_duration); +// +// context.switch_account(&alice); +// let result = context.contract().claim_total(Some(true)); +// +// let PromiseOrValue::Value(ClaimedAmountView::Detailed(value)) = result else { +// panic!(); +// }; +// +// assert_eq!(jar_0_expected_interest + jar_1_expected_interest, value.total.0); +// +// assert_eq!(jar_0_expected_interest, value.detailed.get(&U32(jar_0.id)).unwrap().0); +// assert_eq!(jar_1_expected_interest, value.detailed.get(&U32(jar_1.id)).unwrap().0); +// } +// +// #[test] +// fn claim_pending_withdraw_jar() { +// let alice = alice(); +// let admin = admin(); +// +// let product = Product::new(); +// let jar_0 = Jar::new(0).principal(100_000_000); +// let jar_1 = Jar::new(1).principal(200_000_000).pending_withdraw(); +// +// let mut context = Context::new(admin) +// .with_products(&[product.clone()]) +// .with_jars(&[jar_0.clone(), jar_1.clone()]); +// +// let product_term = product.get_lockup_term().unwrap(); +// let test_duration = product_term + 100; +// +// let jar_0_expected_interest = jar_0.get_interest(&[], &product, test_duration); +// +// context.set_block_timestamp_in_ms(test_duration); +// +// context.switch_account(&alice); +// let result = context.contract().claim_total(Some(true)); +// +// let PromiseOrValue::Value(ClaimedAmountView::Detailed(value)) = result else { +// panic!(); +// }; +// +// assert_eq!(jar_0_expected_interest.0, value.total.0); +// +// assert_eq!(jar_0_expected_interest.0, value.detailed.get(&U32(jar_0.id)).unwrap().0); +// assert_eq!(None, value.detailed.get(&U32(jar_1.id))); +// } +// +// #[test] +// fn dont_delete_jar_on_all_interest_claim() { +// let alice = alice(); +// let admin = admin(); +// +// let product = Product::new().apy(Apy::Constant(UDecimal::new(2, 1))); +// let jar = Jar::new(0); +// let mut context = Context::new(admin).with_products(&[product]).with_jars(&[jar.clone()]); +// +// context.set_block_timestamp_in_days(365); +// +// context.switch_account(&alice); +// context.contract().claim_total(None); +// +// let jar = context.contract().get_jar_internal(&alice, jar.id); +// assert_eq!(200_000, jar.claimed_balance); +// +// let Some(ref cache) = jar.cache else { panic!() }; +// +// assert_eq!(cache.interest, 0); +// assert_eq!(jar.principal, 1_000_000); +// } +// +// #[test] +// #[should_panic(expected = "Jar with id: 0 doesn't exist")] +// fn claim_all_withdraw_all_and_delete_jar() { +// let alice = alice(); +// let admin = admin(); +// +// let product = Product::new().apy(Apy::Constant(UDecimal::new(2, 1))); +// let jar = Jar::new(0); +// +// let jar_id = jar.id; +// +// let mut context = Context::new(admin) +// .with_products(&[product.clone()]) +// .with_jars(&[jar.clone()]); +// +// context.set_block_timestamp_in_ms(product.get_lockup_term().unwrap() + 1); +// +// context.switch_account(&alice); +// let claimed = context.contract().claim_total(None).unwrap(); +// +// assert_eq!(200_000, claimed.get_total().0); +// +// let jar = context.contract().get_jar_internal(&alice, jar_id); +// assert_eq!(200_000, jar.claimed_balance); +// +// let Some(ref cache) = jar.cache else { panic!() }; +// +// assert_eq!(cache.interest, 0); +// assert_eq!(jar.principal, 1_000_000); +// +// let withdrawn = context.contract().withdraw(U32(jar_id), None).unwrap(); +// +// assert_eq!(withdrawn.withdrawn_amount, U128(1_000_000)); +// assert_eq!(withdrawn.fee, U128(0)); +// +// let _jar = context.contract().get_jar_internal(&alice, jar_id); +// } +// +// #[test] +// #[should_panic(expected = "Jar with id: 0 doesn't exist")] +// fn withdraw_all_claim_all_and_delete_jar() { +// let alice = alice(); +// let admin = admin(); +// +// let product = Product::new().apy(Apy::Constant(UDecimal::new(2, 1))); +// let jar = Jar::new(0); +// +// let jar_id = jar.id; +// +// let mut context = Context::new(admin) +// .with_products(&[product.clone()]) +// .with_jars(&[jar.clone()]); +// +// context.set_block_timestamp_in_ms(product.get_lockup_term().unwrap() + 1); +// +// context.switch_account(&alice); +// +// let withdrawn = context.contract().withdraw(U32(jar_id), None).unwrap(); +// +// assert_eq!(withdrawn.withdrawn_amount, U128(1_000_000)); +// assert_eq!(withdrawn.fee, U128(0)); +// +// let jar = context.contract().get_jar_internal(&alice, jar_id); +// +// assert_eq!(jar.principal, 0); +// +// let claimed = context.contract().claim_total(None).unwrap(); +// +// assert_eq!(claimed.get_total(), U128(200_000)); +// +// let _jar = context.contract().get_jar_internal(&alice, jar_id); +// } +// +// #[test] +// fn failed_future_claim() { +// set_test_future_success(false); +// +// let alice = alice(); +// let admin = admin(); +// +// let product = Product::new().apy(Apy::Constant(UDecimal::new(2, 1))); +// let jar = Jar::new(0); +// let mut context = Context::new(admin).with_products(&[product]).with_jars(&[jar.clone()]); +// +// context.set_block_timestamp_in_days(365); +// +// context.switch_account(&alice); +// +// let jar_before_claim = context.contract().get_jar_internal(&alice, jar.id).clone(); +// +// let claimed = context.contract().claim_total(None).unwrap(); +// +// assert_eq!(claimed.get_total().0, 0); +// +// let jar_after_claim = context.contract().get_jar_internal(&alice, jar.id); +// +// assert_eq!(jar_before_claim, jar_after_claim); +// } diff --git a/contract/src/common/tests.rs b/contract/src/common/tests.rs index 688f2948..04e7e66f 100644 --- a/contract/src/common/tests.rs +++ b/contract/src/common/tests.rs @@ -11,7 +11,7 @@ use near_contract_standards::fungible_token::Balance; use near_sdk::{env::block_timestamp_ms, test_utils::VMContextBuilder, testing_env, AccountId, NearToken}; use sweat_jar_model::{api::InitApi, jar::JarId, MS_IN_DAY, MS_IN_HOUR, MS_IN_MINUTE}; -use crate::{jar::model::Jar, product::model::Product, test_utils::AfterCatchUnwind, Contract}; +use crate::{jar::model::Jar, product::model::ProductV2, test_utils::AfterCatchUnwind, Contract}; pub(crate) struct Context { contract: Arc>, @@ -51,7 +51,7 @@ impl Context { self.contract.try_lock().expect("Contract is already locked") } - pub(crate) fn with_products(self, products: &[Product]) -> Self { + pub(crate) fn with_products(self, products: &[ProductV2]) -> Self { for product in products { self.contract().products.insert(&product.id, product); } diff --git a/contract/src/ft_receiver.rs b/contract/src/ft_receiver.rs index 67df7bcb..e0078817 100644 --- a/contract/src/ft_receiver.rs +++ b/contract/src/ft_receiver.rs @@ -1,355 +1,355 @@ -use near_contract_standards::fungible_token::receiver::FungibleTokenReceiver; -use near_sdk::{json_types::U128, near, require, serde_json, AccountId, PromiseOrValue}; -use sweat_jar_model::jar::CeFiJar; - -use crate::{jar::model::JarTicket, near_bindgen, Base64VecU8, Contract, ContractExt}; - -/// The `FtMessage` enum represents various commands for actions available via transferring tokens to an account -/// where this contract is deployed, using the payload in `ft_transfer_call`. -#[near(serializers=[json])] -#[serde(tag = "type", content = "data", rename_all = "snake_case")] -pub enum FtMessage { - /// Represents a request to create a new jar for a corresponding product. - Stake(StakeMessage), - - /// Represents a request to create `DeFi` Jars from provided `CeFi` Jars. - Migrate(Vec), -} - -/// The `StakeMessage` struct represents a request to create a new jar for a corresponding product. -#[near(serializers=[json])] -pub struct StakeMessage { - /// Data of the `JarTicket` required for validating the request and specifying the product. - ticket: JarTicket, - - /// An optional ed25519 signature used to verify the authenticity of the request. - signature: Option, - - /// An optional account ID representing the intended owner of the created jar. - receiver_id: Option, -} - -#[near_bindgen] -impl FungibleTokenReceiver for Contract { - fn ft_on_transfer(&mut self, sender_id: AccountId, amount: U128, msg: String) -> PromiseOrValue { - self.assert_from_ft_contract(); - - let ft_message: FtMessage = serde_json::from_str(&msg).expect("Unable to deserialize msg"); - - match ft_message { - FtMessage::Stake(message) => { - let receiver_id = message.receiver_id.unwrap_or(sender_id); - self.deposit(receiver_id, message.ticket, amount, message.signature); - } - FtMessage::Migrate(jars) => { - require!(sender_id == self.manager, "Migration can be performed only by admin"); - - self.migrate_jars(jars, amount); - } - } - - PromiseOrValue::Value(0.into()) - } -} - -#[cfg(test)] -mod tests { - use std::panic::catch_unwind; - - use near_contract_standards::fungible_token::receiver::FungibleTokenReceiver; - use near_sdk::{ - json_types::U128, - serde_json::json, - test_utils::test_env::{alice, bob}, - }; - use sweat_jar_model::{api::JarApi, UDecimal, U32}; - - use crate::{ - common::tests::Context, - jar::model::Jar, - product::{ - helpers::MessageSigner, - model::{Apy, DowngradableApy, Product}, - }, - test_utils::admin, - Contract, - }; - - #[test] - fn transfer_with_create_jar_message() { - let alice = alice(); - let admin = admin(); - - let product = Product::new(); - let mut context = Context::new(admin).with_products(&[product.clone()]); - - let msg = json!({ - "type": "stake", - "data": { - "ticket": { - "product_id": product.id, - "valid_until": "0", - } - } - }); - - context.switch_account_to_ft_contract_account(); - context - .contract() - .ft_on_transfer(alice.clone(), U128(1_000_000), msg.to_string()); - - let jar = context.contract().get_jar(alice, U32(1)); - assert_eq!(jar.id.0, 1); - } - - #[test] - fn transfer_with_duplicate_create_jar_message() { - let alice = alice(); - let admin = admin(); - - let (signer, product) = generate_premium_product_context(); - - let mut context = Context::new(admin).with_products(&[product.clone()]); - - let ticket_amount = 1_000_000u128; - let ticket_valid_until = 100_000_000u64; - let signature = signer.sign_base64( - Contract::get_signature_material( - &context.owner, - &alice, - &product.id, - ticket_amount, - ticket_valid_until, - None, - ) - .as_str(), - ); - - let msg = json!({ - "type": "stake", - "data": { - "ticket": { - "product_id": product.id, - "valid_until": ticket_valid_until.to_string(), - }, - "signature": signature, - } - }); - - context.switch_account_to_ft_contract_account(); - context - .contract() - .ft_on_transfer(alice.clone(), U128(ticket_amount), msg.to_string()); - - let jar = context.contract().get_jar(alice.clone(), U32(1)); - assert_eq!(jar.id.0, 1); - - let result = catch_unwind(move || { - context - .contract() - .ft_on_transfer(alice.clone(), U128(ticket_amount), msg.to_string()) - }); - assert!(result.is_err()); - } - - #[test] - fn transfer_with_top_up_message_for_refillable_product() { - let alice = alice(); - let admin = admin(); - - let product = Product::new().with_allows_top_up(true); - - let initial_jar_principal = 100; - let reference_jar = Jar::new(0).principal(initial_jar_principal); - - let mut context = Context::new(admin) - .with_products(&[product]) - .with_jars(&[reference_jar.clone()]); - - let msg = json!({ - "type": "top_up", - "data": reference_jar.id, - }); - - context.switch_account_to_ft_contract_account(); - let top_up_amount = 700; - context - .contract() - .ft_on_transfer(alice.clone(), U128(top_up_amount), msg.to_string()); - - let jar = context.contract().get_jar(alice, U32(0)); - assert_eq!(initial_jar_principal + top_up_amount, jar.principal.0); - } - - #[test] - #[should_panic(expected = "The product doesn't allow top-ups")] - fn transfer_with_top_up_message_for_not_refillable_product() { - let alice = alice(); - let admin = admin(); - - let product = Product::new().with_allows_top_up(false); - - let reference_jar = Jar::new(0).principal(500); - - let mut context = Context::new(admin) - .with_products(&[product]) - .with_jars(&[reference_jar.clone()]); - - let msg = json!({ - "type": "top_up", - "data": reference_jar.id, - }); - - context.switch_account_to_ft_contract_account(); - context.contract().ft_on_transfer(alice, U128(100), msg.to_string()); - } - - #[test] - fn transfer_with_top_up_message_for_flexible_product() { - let alice = alice(); - let admin = admin(); - - let product = Product::new().flexible(); - - let initial_jar_principal = 100_000; - let reference_jar = Jar::new(0).principal(initial_jar_principal); - - let mut context = Context::new(admin) - .with_products(&[product]) - .with_jars(&[reference_jar.clone()]); - - let msg = json!({ - "type": "top_up", - "data": reference_jar.id, - }); - - context.switch_account_to_ft_contract_account(); - - let top_up_amount = 1_000; - context - .contract() - .ft_on_transfer(alice.clone(), U128(top_up_amount), msg.to_string()); - - let jar = context.contract().get_jar(alice, U32(0)); - assert_eq!(initial_jar_principal + top_up_amount, jar.principal.0); - } - - #[test] - fn transfer_with_migration_message() { - let alice = alice(); - let bob = bob(); - let admin = admin(); - - let product = Product::new(); - let reference_restakable_product = Product::new().id("restakable_product"); - - let mut context = - Context::new(admin.clone()).with_products(&[product.clone(), reference_restakable_product.clone()]); - - let amount_alice = 100; - let amount_bob = 200; - let msg = json!({ - "type": "migrate", - "data": [ - { - "id": "cefi_product_1", - "account_id": alice, - "product_id": product.id, - "principal": amount_alice.to_string(), - "created_at": "0", - }, - { - "id": "cefi_product_2", - "account_id": bob, - "product_id": reference_restakable_product.id, - "principal": amount_bob.to_string(), - "created_at": "0", - }, - ] - }); - - context.switch_account_to_ft_contract_account(); - context - .contract() - .ft_on_transfer(admin, U128(amount_alice + amount_bob), msg.to_string()); - - let alice_jars = context.contract().get_jars_for_account(alice); - assert_eq!(alice_jars.len(), 1); - assert_eq!(alice_jars.first().unwrap().principal.0, amount_alice); - - let bob_jars = context.contract().get_jars_for_account(bob); - assert_eq!(bob_jars.len(), 1); - assert_eq!(bob_jars.first().unwrap().principal.0, amount_bob); - } - - #[test] - #[should_panic(expected = "Migration can be performed only by admin")] - fn transfer_with_migration_message_by_not_admin() { - let alice = alice(); - let admin = admin(); - - let product = Product::new(); - let reference_restakable_product = Product::new().id("restakable_product"); - - let mut context = Context::new(admin).with_products(&[product.clone(), reference_restakable_product]); - - let amount_alice = 1_000; - let msg = json!({ - "type": "migrate", - "data": [ - { - "id": "cefi_product_3", - "account_id": alice, - "product_id": product.id, - "principal": amount_alice.to_string(), - "created_at": "0", - }, - ] - }); - - context.switch_account_to_ft_contract_account(); - context - .contract() - .ft_on_transfer(alice, U128(amount_alice), msg.to_string()); - } - - #[test] - #[should_panic(expected = "Unable to deserialize msg")] - fn transfer_with_unknown_message() { - let alice = alice(); - let admin = admin(); - - let mut context = Context::new(admin); - - context.switch_account_to_ft_contract_account(); - context - .contract() - .ft_on_transfer(alice, U128(300), "something".to_string()); - } - - #[test] - #[should_panic(expected = "Can receive tokens only from token")] - fn transfer_by_not_token_account() { - let alice = alice(); - let admin = admin(); - - let mut context = Context::new(admin); - - context.switch_account(&alice); - context - .contract() - .ft_on_transfer(alice.clone(), U128(300), "something".to_string()); - } - - fn generate_premium_product_context() -> (MessageSigner, Product) { - let signer = MessageSigner::new(); - let product = Product::new() - .public_key(signer.public_key()) - .apy(Apy::Downgradable(DowngradableApy { - default: UDecimal::new(20, 2), - fallback: UDecimal::new(10, 2), - })); - - (signer, product) - } -} +// use near_contract_standards::fungible_token::receiver::FungibleTokenReceiver; +// use near_sdk::{json_types::U128, near, require, serde_json, AccountId, PromiseOrValue}; +// use sweat_jar_model::jar::CeFiJar; +// +// use crate::{jar::model::JarTicket, near_bindgen, Base64VecU8, Contract, ContractExt}; +// +// /// The `FtMessage` enum represents various commands for actions available via transferring tokens to an account +// /// where this contract is deployed, using the payload in `ft_transfer_call`. +// #[near(serializers=[json])] +// #[serde(tag = "type", content = "data", rename_all = "snake_case")] +// pub enum FtMessage { +// /// Represents a request to create a new jar for a corresponding product. +// Stake(StakeMessage), +// +// /// Represents a request to create `DeFi` Jars from provided `CeFi` Jars. +// Migrate(Vec), +// } +// +// /// The `StakeMessage` struct represents a request to create a new jar for a corresponding product. +// #[near(serializers=[json])] +// pub struct StakeMessage { +// /// Data of the `JarTicket` required for validating the request and specifying the product. +// ticket: JarTicket, +// +// /// An optional ed25519 signature used to verify the authenticity of the request. +// signature: Option, +// +// /// An optional account ID representing the intended owner of the created jar. +// receiver_id: Option, +// } +// +// #[near_bindgen] +// impl FungibleTokenReceiver for Contract { +// fn ft_on_transfer(&mut self, sender_id: AccountId, amount: U128, msg: String) -> PromiseOrValue { +// self.assert_from_ft_contract(); +// +// let ft_message: FtMessage = serde_json::from_str(&msg).expect("Unable to deserialize msg"); +// +// match ft_message { +// FtMessage::Stake(message) => { +// let receiver_id = message.receiver_id.unwrap_or(sender_id); +// self.deposit(receiver_id, message.ticket, amount, message.signature); +// } +// FtMessage::Migrate(jars) => { +// require!(sender_id == self.manager, "Migration can be performed only by admin"); +// +// self.migrate_jars(jars, amount); +// } +// } +// +// PromiseOrValue::Value(0.into()) +// } +// } +// +// #[cfg(test)] +// mod tests { +// use std::panic::catch_unwind; +// +// use near_contract_standards::fungible_token::receiver::FungibleTokenReceiver; +// use near_sdk::{ +// json_types::U128, +// serde_json::json, +// test_utils::test_env::{alice, bob}, +// }; +// use sweat_jar_model::{api::JarApi, UDecimal, U32}; +// +// use crate::{ +// common::tests::Context, +// jar::model::Jar, +// product::{ +// helpers::MessageSigner, +// model::{Apy, DowngradableApy, Product}, +// }, +// test_utils::admin, +// Contract, +// }; +// +// #[test] +// fn transfer_with_create_jar_message() { +// let alice = alice(); +// let admin = admin(); +// +// let product = Product::new(); +// let mut context = Context::new(admin).with_products(&[product.clone()]); +// +// let msg = json!({ +// "type": "stake", +// "data": { +// "ticket": { +// "product_id": product.id, +// "valid_until": "0", +// } +// } +// }); +// +// context.switch_account_to_ft_contract_account(); +// context +// .contract() +// .ft_on_transfer(alice.clone(), U128(1_000_000), msg.to_string()); +// +// let jar = context.contract().get_jar(alice, U32(1)); +// assert_eq!(jar.id.0, 1); +// } +// +// #[test] +// fn transfer_with_duplicate_create_jar_message() { +// let alice = alice(); +// let admin = admin(); +// +// let (signer, product) = generate_premium_product_context(); +// +// let mut context = Context::new(admin).with_products(&[product.clone()]); +// +// let ticket_amount = 1_000_000u128; +// let ticket_valid_until = 100_000_000u64; +// let signature = signer.sign_base64( +// Contract::get_signature_material( +// &context.owner, +// &alice, +// &product.id, +// ticket_amount, +// ticket_valid_until, +// None, +// ) +// .as_str(), +// ); +// +// let msg = json!({ +// "type": "stake", +// "data": { +// "ticket": { +// "product_id": product.id, +// "valid_until": ticket_valid_until.to_string(), +// }, +// "signature": signature, +// } +// }); +// +// context.switch_account_to_ft_contract_account(); +// context +// .contract() +// .ft_on_transfer(alice.clone(), U128(ticket_amount), msg.to_string()); +// +// let jar = context.contract().get_jar(alice.clone(), U32(1)); +// assert_eq!(jar.id.0, 1); +// +// let result = catch_unwind(move || { +// context +// .contract() +// .ft_on_transfer(alice.clone(), U128(ticket_amount), msg.to_string()) +// }); +// assert!(result.is_err()); +// } +// +// #[test] +// fn transfer_with_top_up_message_for_refillable_product() { +// let alice = alice(); +// let admin = admin(); +// +// let product = Product::new().with_allows_top_up(true); +// +// let initial_jar_principal = 100; +// let reference_jar = Jar::new(0).principal(initial_jar_principal); +// +// let mut context = Context::new(admin) +// .with_products(&[product]) +// .with_jars(&[reference_jar.clone()]); +// +// let msg = json!({ +// "type": "top_up", +// "data": reference_jar.id, +// }); +// +// context.switch_account_to_ft_contract_account(); +// let top_up_amount = 700; +// context +// .contract() +// .ft_on_transfer(alice.clone(), U128(top_up_amount), msg.to_string()); +// +// let jar = context.contract().get_jar(alice, U32(0)); +// assert_eq!(initial_jar_principal + top_up_amount, jar.principal.0); +// } +// +// #[test] +// #[should_panic(expected = "The product doesn't allow top-ups")] +// fn transfer_with_top_up_message_for_not_refillable_product() { +// let alice = alice(); +// let admin = admin(); +// +// let product = Product::new().with_allows_top_up(false); +// +// let reference_jar = Jar::new(0).principal(500); +// +// let mut context = Context::new(admin) +// .with_products(&[product]) +// .with_jars(&[reference_jar.clone()]); +// +// let msg = json!({ +// "type": "top_up", +// "data": reference_jar.id, +// }); +// +// context.switch_account_to_ft_contract_account(); +// context.contract().ft_on_transfer(alice, U128(100), msg.to_string()); +// } +// +// #[test] +// fn transfer_with_top_up_message_for_flexible_product() { +// let alice = alice(); +// let admin = admin(); +// +// let product = Product::new().flexible(); +// +// let initial_jar_principal = 100_000; +// let reference_jar = Jar::new(0).principal(initial_jar_principal); +// +// let mut context = Context::new(admin) +// .with_products(&[product]) +// .with_jars(&[reference_jar.clone()]); +// +// let msg = json!({ +// "type": "top_up", +// "data": reference_jar.id, +// }); +// +// context.switch_account_to_ft_contract_account(); +// +// let top_up_amount = 1_000; +// context +// .contract() +// .ft_on_transfer(alice.clone(), U128(top_up_amount), msg.to_string()); +// +// let jar = context.contract().get_jar(alice, U32(0)); +// assert_eq!(initial_jar_principal + top_up_amount, jar.principal.0); +// } +// +// #[test] +// fn transfer_with_migration_message() { +// let alice = alice(); +// let bob = bob(); +// let admin = admin(); +// +// let product = Product::new(); +// let reference_restakable_product = Product::new().id("restakable_product"); +// +// let mut context = +// Context::new(admin.clone()).with_products(&[product.clone(), reference_restakable_product.clone()]); +// +// let amount_alice = 100; +// let amount_bob = 200; +// let msg = json!({ +// "type": "migrate", +// "data": [ +// { +// "id": "cefi_product_1", +// "account_id": alice, +// "product_id": product.id, +// "principal": amount_alice.to_string(), +// "created_at": "0", +// }, +// { +// "id": "cefi_product_2", +// "account_id": bob, +// "product_id": reference_restakable_product.id, +// "principal": amount_bob.to_string(), +// "created_at": "0", +// }, +// ] +// }); +// +// context.switch_account_to_ft_contract_account(); +// context +// .contract() +// .ft_on_transfer(admin, U128(amount_alice + amount_bob), msg.to_string()); +// +// let alice_jars = context.contract().get_jars_for_account(alice); +// assert_eq!(alice_jars.len(), 1); +// assert_eq!(alice_jars.first().unwrap().principal.0, amount_alice); +// +// let bob_jars = context.contract().get_jars_for_account(bob); +// assert_eq!(bob_jars.len(), 1); +// assert_eq!(bob_jars.first().unwrap().principal.0, amount_bob); +// } +// +// #[test] +// #[should_panic(expected = "Migration can be performed only by admin")] +// fn transfer_with_migration_message_by_not_admin() { +// let alice = alice(); +// let admin = admin(); +// +// let product = Product::new(); +// let reference_restakable_product = Product::new().id("restakable_product"); +// +// let mut context = Context::new(admin).with_products(&[product.clone(), reference_restakable_product]); +// +// let amount_alice = 1_000; +// let msg = json!({ +// "type": "migrate", +// "data": [ +// { +// "id": "cefi_product_3", +// "account_id": alice, +// "product_id": product.id, +// "principal": amount_alice.to_string(), +// "created_at": "0", +// }, +// ] +// }); +// +// context.switch_account_to_ft_contract_account(); +// context +// .contract() +// .ft_on_transfer(alice, U128(amount_alice), msg.to_string()); +// } +// +// #[test] +// #[should_panic(expected = "Unable to deserialize msg")] +// fn transfer_with_unknown_message() { +// let alice = alice(); +// let admin = admin(); +// +// let mut context = Context::new(admin); +// +// context.switch_account_to_ft_contract_account(); +// context +// .contract() +// .ft_on_transfer(alice, U128(300), "something".to_string()); +// } +// +// #[test] +// #[should_panic(expected = "Can receive tokens only from token")] +// fn transfer_by_not_token_account() { +// let alice = alice(); +// let admin = admin(); +// +// let mut context = Context::new(admin); +// +// context.switch_account(&alice); +// context +// .contract() +// .ft_on_transfer(alice.clone(), U128(300), "something".to_string()); +// } +// +// fn generate_premium_product_context() -> (MessageSigner, Product) { +// let signer = MessageSigner::new(); +// let product = Product::new() +// .public_key(signer.public_key()) +// .apy(Apy::Downgradable(DowngradableApy { +// default: UDecimal::new(20, 2), +// fallback: UDecimal::new(10, 2), +// })); +// +// (signer, product) +// } +// } diff --git a/contract/src/jar/account/v2.rs b/contract/src/jar/account/v2.rs index 06379220..3651cf0a 100644 --- a/contract/src/jar/account/v2.rs +++ b/contract/src/jar/account/v2.rs @@ -57,6 +57,12 @@ impl Contract { } impl AccountV2 { + pub(crate) fn get_jar(&self, product_id: &ProductId) -> &JarV2 { + self.jars + .get(product_id) + .unwrap_or_else(|| env::panic_str(format!("Jar for product {product_id} is not found").as_str())) + } + pub(crate) fn get_jar_mut(&mut self, product_id: &ProductId) -> &mut JarV2 { self.jars .get_mut(product_id) @@ -116,8 +122,9 @@ impl AccountV2 { } impl Contract { - pub(crate) fn update_account_cache(&mut self, account: &mut AccountV2) { + pub(crate) fn update_account_cache(&mut self, account_id: &AccountId) { let now = env::block_timestamp_ms(); + let account = self.get_account_mut(account_id); for (product_id, jar) in account.jars.iter_mut() { let product = &self.get_product(product_id); @@ -125,7 +132,8 @@ impl Contract { } } - pub(crate) fn update_jar_cache(&mut self, account: &mut AccountV2, product_id: &ProductId) { + pub(crate) fn update_jar_cache(&mut self, account_id: &AccountId, product_id: &ProductId) { + let account = self.get_account_mut(account_id); let product = &self.get_product(product_id); let jar = account.get_jar_mut(product_id); jar.update_cache(account, product, env::block_timestamp_ms()); diff --git a/contract/src/jar/model/v2.rs b/contract/src/jar/model/v2.rs index e1614b57..a79b6c52 100644 --- a/contract/src/jar/model/v2.rs +++ b/contract/src/jar/model/v2.rs @@ -2,6 +2,7 @@ use near_sdk::near; use sweat_jar_model::TokenAmount; use crate::{ + assert::assert_not_locked, common::{Duration, Timestamp}, jar::model::JarCache, product::model::v2::Terms, @@ -65,6 +66,11 @@ impl JarV2 { self } + pub(crate) fn try_lock(&mut self) -> &mut Self { + assert_not_locked(self); + self.lock() + } + pub(crate) fn unlock(&mut self) -> &mut Self { self.is_pending_withdraw = false; diff --git a/contract/src/jar/tests/restake.rs b/contract/src/jar/tests/restake.rs index 9af9c1a4..d405fd8e 100644 --- a/contract/src/jar/tests/restake.rs +++ b/contract/src/jar/tests/restake.rs @@ -1,154 +1,154 @@ -use near_sdk::test_utils::test_env::{alice, bob, carol}; -use sweat_jar_model::{ - api::{JarApi, ProductApi}, - U32, -}; - -use crate::{ - common::tests::Context, - jar::model::Jar, - product::model::Product, - test_utils::{admin, expect_panic}, -}; - -#[test] -fn restake_by_not_owner() { - let product = Product::new().with_allows_restaking(true); - let alice_jar = Jar::new(0); - let mut ctx = Context::new(admin()) - .with_products(&[product]) - .with_jars(&[alice_jar.clone()]); - - ctx.switch_account(bob()); - expect_panic(&ctx, "Account 'bob.near' doesn't exist", || { - ctx.contract().restake(U32(alice_jar.id)); - }); - - expect_panic(&ctx, "Jars for account bob.near don't exist", || { - ctx.contract().restake_all(None); - }); - - ctx.switch_account(carol()); - expect_panic(&ctx, "Account 'carol.near' doesn't exist", || { - ctx.contract().restake(U32(alice_jar.id)); - }); - - expect_panic(&ctx, "Jars for account carol.near don't exist", || { - ctx.contract().restake_all(None); - }); -} - -#[test] -#[should_panic(expected = "The jar is not mature yet")] -fn restake_before_maturity() { - let alice = alice(); - let admin = admin(); - - let product = Product::new().with_allows_restaking(true); - let jar = Jar::new(0); - let mut context = Context::new(admin).with_products(&[product]).with_jars(&[jar.clone()]); - - context.switch_account(&alice); - assert!(context.contract().restake_all(None).is_empty()); - context.contract().restake(U32(jar.id)); -} - -#[test] -#[should_panic(expected = "The product doesn't support restaking")] -fn restake_when_restaking_is_not_supported() { - let alice = alice(); - let admin = admin(); - - let product = Product::new().with_allows_restaking(false); - - let jar = Jar::new(0); - let mut context = Context::new(admin).with_products(&[product]).with_jars(&[jar.clone()]); - - context.switch_account(&alice); - assert!(context.contract().restake_all(None).is_empty()); - context.contract().restake(U32(jar.id)); -} - -#[test] -#[should_panic(expected = "The product is disabled")] -fn restake_with_disabled_product() { - let alice = alice(); - let admin = admin(); - - let product = Product::new().with_allows_restaking(true); - let jar = Jar::new(0); - let mut context = Context::new(admin.clone()) - .with_products(&[product.clone()]) - .with_jars(&[jar.clone()]); - - context.switch_account(&admin); - context.with_deposit_yocto(1, |context| context.contract().set_enabled(product.id, false)); - - context.contract().products_cache.borrow_mut().clear(); - - context.set_block_timestamp_in_days(366); - - context.switch_account(&alice); - assert!(context.contract().restake_all(None).is_empty()); - context.contract().restake(U32(jar.id)); -} - -#[test] -#[should_panic(expected = "The jar is empty, nothing to restake")] -fn restake_empty_jar() { - let alice = alice(); - let admin = admin(); - - let product = Product::new().with_allows_restaking(true); - let jar = Jar::new(0).principal(0); - let mut context = Context::new(admin).with_products(&[product]).with_jars(&[jar.clone()]); - - context.set_block_timestamp_in_days(366); - - context.switch_account(&alice); - assert!(context.contract().restake_all(None).is_empty()); - context.contract().restake(U32(jar.id)); -} - -#[test] -fn restake_after_maturity_for_restakable_product() { - let alice = alice(); - let admin = admin(); - - let product = Product::new().with_allows_restaking(true); - let jar = Jar::new(0); - let mut context = Context::new(admin).with_products(&[product]).with_jars(&[jar.clone()]); - - context.set_block_timestamp_in_days(366); - - context.switch_account(&alice); - context.contract().restake(U32(jar.id)); - - let alice_jars = context.contract().get_jars_for_account(alice); - - assert_eq!(2, alice_jars.len()); - assert_eq!(0, alice_jars.iter().find(|item| item.id.0 == 0).unwrap().principal.0); - assert_eq!( - 1_000_000, - alice_jars.iter().find(|item| item.id.0 == 1).unwrap().principal.0 - ); -} - -#[test] -#[should_panic(expected = "The product doesn't support restaking")] -fn restake_after_maturity_for_not_restakable_product() { - let alice = alice(); - let admin = admin(); - - let product = Product::new().with_allows_restaking(false); - let jar = Jar::new(0); - let mut context = Context::new(admin.clone()) - .with_products(&[product.clone()]) - .with_jars(&[jar.clone()]); - - context.set_block_timestamp_in_days(366); - - context.switch_account(&alice); - assert!(context.contract().restake_all(None).is_empty()); - context.contract().restake(U32(jar.id)); -} +// use near_sdk::test_utils::test_env::{alice, bob, carol}; +// use sweat_jar_model::{ +// api::{JarApi, ProductApi}, +// U32, +// }; +// +// use crate::{ +// common::tests::Context, +// jar::model::Jar, +// product::model::Product, +// test_utils::{admin, expect_panic}, +// }; +// +// #[test] +// fn restake_by_not_owner() { +// let product = Product::new().with_allows_restaking(true); +// let alice_jar = Jar::new(0); +// let mut ctx = Context::new(admin()) +// .with_products(&[product]) +// .with_jars(&[alice_jar.clone()]); +// +// ctx.switch_account(bob()); +// expect_panic(&ctx, "Account 'bob.near' doesn't exist", || { +// ctx.contract().restake(U32(alice_jar.id)); +// }); +// +// expect_panic(&ctx, "Jars for account bob.near don't exist", || { +// ctx.contract().restake_all(None); +// }); +// +// ctx.switch_account(carol()); +// expect_panic(&ctx, "Account 'carol.near' doesn't exist", || { +// ctx.contract().restake(U32(alice_jar.id)); +// }); +// +// expect_panic(&ctx, "Jars for account carol.near don't exist", || { +// ctx.contract().restake_all(None); +// }); +// } +// +// #[test] +// #[should_panic(expected = "The jar is not mature yet")] +// fn restake_before_maturity() { +// let alice = alice(); +// let admin = admin(); +// +// let product = Product::new().with_allows_restaking(true); +// let jar = Jar::new(0); +// let mut context = Context::new(admin).with_products(&[product]).with_jars(&[jar.clone()]); +// +// context.switch_account(&alice); +// assert!(context.contract().restake_all(None).is_empty()); +// context.contract().restake(U32(jar.id)); +// } +// +// #[test] +// #[should_panic(expected = "The product doesn't support restaking")] +// fn restake_when_restaking_is_not_supported() { +// let alice = alice(); +// let admin = admin(); +// +// let product = Product::new().with_allows_restaking(false); +// +// let jar = Jar::new(0); +// let mut context = Context::new(admin).with_products(&[product]).with_jars(&[jar.clone()]); +// +// context.switch_account(&alice); +// assert!(context.contract().restake_all(None).is_empty()); +// context.contract().restake(U32(jar.id)); +// } +// +// #[test] +// #[should_panic(expected = "The product is disabled")] +// fn restake_with_disabled_product() { +// let alice = alice(); +// let admin = admin(); +// +// let product = Product::new().with_allows_restaking(true); +// let jar = Jar::new(0); +// let mut context = Context::new(admin.clone()) +// .with_products(&[product.clone()]) +// .with_jars(&[jar.clone()]); +// +// context.switch_account(&admin); +// context.with_deposit_yocto(1, |context| context.contract().set_enabled(product.id, false)); +// +// context.contract().products_cache.borrow_mut().clear(); +// +// context.set_block_timestamp_in_days(366); +// +// context.switch_account(&alice); +// assert!(context.contract().restake_all(None).is_empty()); +// context.contract().restake(U32(jar.id)); +// } +// +// #[test] +// #[should_panic(expected = "The jar is empty, nothing to restake")] +// fn restake_empty_jar() { +// let alice = alice(); +// let admin = admin(); +// +// let product = Product::new().with_allows_restaking(true); +// let jar = Jar::new(0).principal(0); +// let mut context = Context::new(admin).with_products(&[product]).with_jars(&[jar.clone()]); +// +// context.set_block_timestamp_in_days(366); +// +// context.switch_account(&alice); +// assert!(context.contract().restake_all(None).is_empty()); +// context.contract().restake(U32(jar.id)); +// } +// +// #[test] +// fn restake_after_maturity_for_restakable_product() { +// let alice = alice(); +// let admin = admin(); +// +// let product = Product::new().with_allows_restaking(true); +// let jar = Jar::new(0); +// let mut context = Context::new(admin).with_products(&[product]).with_jars(&[jar.clone()]); +// +// context.set_block_timestamp_in_days(366); +// +// context.switch_account(&alice); +// context.contract().restake(U32(jar.id)); +// +// let alice_jars = context.contract().get_jars_for_account(alice); +// +// assert_eq!(2, alice_jars.len()); +// assert_eq!(0, alice_jars.iter().find(|item| item.id.0 == 0).unwrap().principal.0); +// assert_eq!( +// 1_000_000, +// alice_jars.iter().find(|item| item.id.0 == 1).unwrap().principal.0 +// ); +// } +// +// #[test] +// #[should_panic(expected = "The product doesn't support restaking")] +// fn restake_after_maturity_for_not_restakable_product() { +// let alice = alice(); +// let admin = admin(); +// +// let product = Product::new().with_allows_restaking(false); +// let jar = Jar::new(0); +// let mut context = Context::new(admin.clone()) +// .with_products(&[product.clone()]) +// .with_jars(&[jar.clone()]); +// +// context.set_block_timestamp_in_days(366); +// +// context.switch_account(&alice); +// assert!(context.contract().restake_all(None).is_empty()); +// context.contract().restake(U32(jar.id)); +// } diff --git a/contract/src/jar/tests/restake_all.rs b/contract/src/jar/tests/restake_all.rs index bf7ffd12..2953bbbf 100644 --- a/contract/src/jar/tests/restake_all.rs +++ b/contract/src/jar/tests/restake_all.rs @@ -1,154 +1,154 @@ -use near_sdk::test_utils::test_env::alice; -use sweat_jar_model::{ - api::{ClaimApi, JarApi}, - MS_IN_YEAR, -}; - -use crate::{ - common::tests::Context, - jar::model::Jar, - product::model::Product, - test_utils::{admin, PRINCIPAL}, -}; - -#[test] -fn restake_all() { - let alice = alice(); - let admin = admin(); - - let restakable_product = Product::new().id("restakable_product").with_allows_restaking(true); - - let disabled_restakable_product = Product::new() - .id("disabled_restakable_product") - .with_allows_restaking(true) - .enabled(false); - - let non_restakable_product = Product::new().id("non_restakable_product").with_allows_restaking(false); - - let long_term_restakable_product = Product::new() - .id("long_term_restakable_product") - .with_allows_restaking(true) - .lockup_term(MS_IN_YEAR * 2); - - let restakable_jar_1 = Jar::new(0).product_id(&restakable_product.id).principal(PRINCIPAL); - let restakable_jar_2 = Jar::new(1).product_id(&restakable_product.id).principal(PRINCIPAL); - - let disabled_jar = Jar::new(2) - .product_id(&disabled_restakable_product.id) - .principal(PRINCIPAL); - - let non_restakable_jar = Jar::new(3).product_id(&non_restakable_product.id).principal(PRINCIPAL); - - let long_term_jar = Jar::new(4) - .product_id(&long_term_restakable_product.id) - .principal(PRINCIPAL); - - let mut context = Context::new(admin) - .with_products(&[ - restakable_product, - disabled_restakable_product, - non_restakable_product, - long_term_restakable_product, - ]) - .with_jars(&[ - restakable_jar_1.clone(), - restakable_jar_2.clone(), - disabled_jar.clone(), - non_restakable_jar.clone(), - long_term_jar.clone(), - ]); - - context.set_block_timestamp_in_days(366); - - context.switch_account(&alice); - - let restaked_jars = context.contract().restake_all(None); - - assert_eq!(restaked_jars.len(), 2); - assert_eq!( - restaked_jars.iter().map(|j| j.id.0).collect::>(), - // 4 was last jar is, so 2 new restaked jars will have ids 5 and 6 - vec![5, 6] - ); - - let all_jars = context.contract().get_jars_for_account(alice); - - assert_eq!( - all_jars.iter().map(|j| j.id.0).collect::>(), - [ - restakable_jar_1.id, - restakable_jar_2.id, - disabled_jar.id, - non_restakable_jar.id, - long_term_jar.id, - 5, - 6, - ] - ) -} - -#[test] -fn restake_all_after_maturity_for_restakable_product_one_jar() { - let alice = alice(); - let admin = admin(); - - let product = Product::new().with_allows_restaking(true); - let jar = Jar::new(0).principal(PRINCIPAL); - let mut context = Context::new(admin).with_products(&[product]).with_jars(&[jar.clone()]); - - context.set_block_timestamp_in_days(366); - - context.switch_account(&alice); - let restaked = context.contract().restake_all(None).into_iter().next().unwrap(); - - assert_eq!(restaked.account_id, alice); - assert_eq!(restaked.principal.0, PRINCIPAL); - assert_eq!(restaked.claimed_balance.0, 0); - - let alice_jars = context.contract().get_jars_for_account(alice); - - assert_eq!(2, alice_jars.len()); - assert_eq!(0, alice_jars.iter().find(|item| item.id.0 == 0).unwrap().principal.0); - assert_eq!( - PRINCIPAL, - alice_jars.iter().find(|item| item.id.0 == 1).unwrap().principal.0 - ); -} - -#[test] -fn batch_restake_all() { - let alice = alice(); - let admin = admin(); - - let product = Product::new().with_allows_restaking(true).lockup_term(MS_IN_YEAR); - - let jars: Vec<_> = (0..8) - .map(|id| Jar::new(id).principal(PRINCIPAL + id as u128)) - .collect(); - - let mut context = Context::new(admin).with_products(&[product]).with_jars(&jars); - - context.set_block_timestamp_in_days(366); - - context.switch_account(&alice); - - context.contract().claim_total(None); - - let restaked: Vec<_> = context - .contract() - .restake_all(Some(vec![1.into(), 2.into(), 5.into()])) - .into_iter() - .map(|j| j.id.0) - .collect(); - - assert_eq!(restaked, [8, 9, 10]); - - let jars: Vec<_> = context - .contract() - .get_jars_for_account(alice) - .into_iter() - .map(|j| j.id.0) - .collect(); - - assert_eq!(jars, [0, 7, 8, 3, 4, 9, 6, 10,]); -} +// use near_sdk::test_utils::test_env::alice; +// use sweat_jar_model::{ +// api::{ClaimApi, JarApi}, +// MS_IN_YEAR, +// }; +// +// use crate::{ +// common::tests::Context, +// jar::model::Jar, +// product::model::Product, +// test_utils::{admin, PRINCIPAL}, +// }; +// +// #[test] +// fn restake_all() { +// let alice = alice(); +// let admin = admin(); +// +// let restakable_product = Product::new().id("restakable_product").with_allows_restaking(true); +// +// let disabled_restakable_product = Product::new() +// .id("disabled_restakable_product") +// .with_allows_restaking(true) +// .enabled(false); +// +// let non_restakable_product = Product::new().id("non_restakable_product").with_allows_restaking(false); +// +// let long_term_restakable_product = Product::new() +// .id("long_term_restakable_product") +// .with_allows_restaking(true) +// .lockup_term(MS_IN_YEAR * 2); +// +// let restakable_jar_1 = Jar::new(0).product_id(&restakable_product.id).principal(PRINCIPAL); +// let restakable_jar_2 = Jar::new(1).product_id(&restakable_product.id).principal(PRINCIPAL); +// +// let disabled_jar = Jar::new(2) +// .product_id(&disabled_restakable_product.id) +// .principal(PRINCIPAL); +// +// let non_restakable_jar = Jar::new(3).product_id(&non_restakable_product.id).principal(PRINCIPAL); +// +// let long_term_jar = Jar::new(4) +// .product_id(&long_term_restakable_product.id) +// .principal(PRINCIPAL); +// +// let mut context = Context::new(admin) +// .with_products(&[ +// restakable_product, +// disabled_restakable_product, +// non_restakable_product, +// long_term_restakable_product, +// ]) +// .with_jars(&[ +// restakable_jar_1.clone(), +// restakable_jar_2.clone(), +// disabled_jar.clone(), +// non_restakable_jar.clone(), +// long_term_jar.clone(), +// ]); +// +// context.set_block_timestamp_in_days(366); +// +// context.switch_account(&alice); +// +// let restaked_jars = context.contract().restake_all(None); +// +// assert_eq!(restaked_jars.len(), 2); +// assert_eq!( +// restaked_jars.iter().map(|j| j.id.0).collect::>(), +// // 4 was last jar is, so 2 new restaked jars will have ids 5 and 6 +// vec![5, 6] +// ); +// +// let all_jars = context.contract().get_jars_for_account(alice); +// +// assert_eq!( +// all_jars.iter().map(|j| j.id.0).collect::>(), +// [ +// restakable_jar_1.id, +// restakable_jar_2.id, +// disabled_jar.id, +// non_restakable_jar.id, +// long_term_jar.id, +// 5, +// 6, +// ] +// ) +// } +// +// #[test] +// fn restake_all_after_maturity_for_restakable_product_one_jar() { +// let alice = alice(); +// let admin = admin(); +// +// let product = Product::new().with_allows_restaking(true); +// let jar = Jar::new(0).principal(PRINCIPAL); +// let mut context = Context::new(admin).with_products(&[product]).with_jars(&[jar.clone()]); +// +// context.set_block_timestamp_in_days(366); +// +// context.switch_account(&alice); +// let restaked = context.contract().restake_all(None).into_iter().next().unwrap(); +// +// assert_eq!(restaked.account_id, alice); +// assert_eq!(restaked.principal.0, PRINCIPAL); +// assert_eq!(restaked.claimed_balance.0, 0); +// +// let alice_jars = context.contract().get_jars_for_account(alice); +// +// assert_eq!(2, alice_jars.len()); +// assert_eq!(0, alice_jars.iter().find(|item| item.id.0 == 0).unwrap().principal.0); +// assert_eq!( +// PRINCIPAL, +// alice_jars.iter().find(|item| item.id.0 == 1).unwrap().principal.0 +// ); +// } +// +// #[test] +// fn batch_restake_all() { +// let alice = alice(); +// let admin = admin(); +// +// let product = Product::new().with_allows_restaking(true).lockup_term(MS_IN_YEAR); +// +// let jars: Vec<_> = (0..8) +// .map(|id| Jar::new(id).principal(PRINCIPAL + id as u128)) +// .collect(); +// +// let mut context = Context::new(admin).with_products(&[product]).with_jars(&jars); +// +// context.set_block_timestamp_in_days(366); +// +// context.switch_account(&alice); +// +// context.contract().claim_total(None); +// +// let restaked: Vec<_> = context +// .contract() +// .restake_all(Some(vec![1.into(), 2.into(), 5.into()])) +// .into_iter() +// .map(|j| j.id.0) +// .collect(); +// +// assert_eq!(restaked, [8, 9, 10]); +// +// let jars: Vec<_> = context +// .contract() +// .get_jars_for_account(alice) +// .into_iter() +// .map(|j| j.id.0) +// .collect(); +// +// assert_eq!(jars, [0, 7, 8, 3, 4, 9, 6, 10,]); +// } diff --git a/contract/src/jar/tests/tests.rs b/contract/src/jar/tests/tests.rs index 64ac4137..f11f22cb 100644 --- a/contract/src/jar/tests/tests.rs +++ b/contract/src/jar/tests/tests.rs @@ -1,242 +1,242 @@ -#![cfg(test)] - -use fake::Fake; -use near_sdk::Timestamp; -use sweat_jar_model::{UDecimal, MS_IN_YEAR}; - -use crate::{ - product::model::{Apy, Product}, - Jar, -}; - -#[test] -fn get_interest_before_maturity() { - let product = Product::new().lockup_term(2 * MS_IN_YEAR); - let jar = Jar::new(0).principal(100_000_000); - - let interest = jar.get_interest(&[], &product, MS_IN_YEAR).0; - assert_eq!(12_000_000, interest); -} - -#[test] -fn get_interest_after_maturity() { - let product = Product::new(); - let jar = Jar::new(0).principal(100_000_000); - - let interest = jar.get_interest(&[], &product, 400 * 24 * 60 * 60 * 1000).0; - assert_eq!(12_000_000, interest); -} - -#[test] -fn interest_precision() { - let product = Product::new().apy(Apy::Constant(UDecimal::new(1, 0))); - let jar = Jar::new(0).principal(MS_IN_YEAR as u128); - - assert_eq!(jar.get_interest(&[], &product, 10000000000).0, 10000000000); - assert_eq!(jar.get_interest(&[], &product, 10000000001).0, 10000000001); - - for _ in 0..100 { - let time: Timestamp = (10..MS_IN_YEAR).fake(); - assert_eq!(jar.get_interest(&[], &product, time).0, time as u128); - } -} - -#[cfg(test)] -mod signature_tests { - - use near_sdk::{ - json_types::{Base64VecU8, U128, U64}, - test_utils::test_env::alice, - }; - - use crate::{ - common::tests::Context, - jar::model::JarTicket, - product::{helpers::MessageSigner, model::Product}, - test_utils::{admin, generate_premium_product}, - }; - - #[test] - fn verify_ticket_with_valid_signature_and_date() { - let admin = admin(); - - let signer = MessageSigner::new(); - let product = generate_premium_product("premium_product", &signer); - let context = Context::new(admin.clone()).with_products(&[product.clone()]); - - let amount = 14_000_000; - let ticket = JarTicket { - product_id: product.id, - valid_until: U64(123000000), - timezone: None, - }; - - let signature = signer.sign(context.get_signature_material(&admin, &ticket, amount).as_str()); - - context - .contract() - .verify(&admin, amount, &ticket, Some(Base64VecU8(signature))); - } - - #[test] - #[should_panic(expected = "Signature must be 64 bytes")] - fn verify_ticket_with_invalid_signature() { - let alice = alice(); - let admin = admin(); - - let signer = MessageSigner::new(); - let product = generate_premium_product("premium_product", &signer); - let context = Context::new(admin).with_products(&[product.clone()]); - - let amount = 1_000_000; - let ticket = JarTicket { - product_id: product.id, - valid_until: U64(100000000), - timezone: None, - }; - - let signature: Vec = vec![0, 1, 2]; - - context - .contract() - .verify(&alice, amount, &ticket, Some(Base64VecU8(signature))); - } - - #[test] - #[should_panic(expected = "Not matching signature")] - fn verify_ticket_with_not_matching_signature() { - let admin = admin(); - - let signer = MessageSigner::new(); - let product = generate_premium_product("premium_product", &signer); - let another_product = generate_premium_product("another_premium_product", &MessageSigner::new()); - - let context = Context::new(admin.clone()).with_products(&[product, another_product.clone()]); - - let amount = 15_000_000; - let ticket_for_another_product = JarTicket { - product_id: another_product.id, - valid_until: U64(100000000), - timezone: None, - }; - - // signature made for wrong product - let signature = signer.sign( - context - .get_signature_material(&admin, &ticket_for_another_product, amount) - .as_str(), - ); - - context.contract().verify( - &admin, - amount, - &ticket_for_another_product, - Some(Base64VecU8(signature)), - ); - } - - #[test] - #[should_panic(expected = "Ticket is outdated")] - fn verify_ticket_with_invalid_date() { - let alice = alice(); - let admin = admin(); - - let signer = MessageSigner::new(); - let product = generate_premium_product("premium_product", &signer); - let mut context = Context::new(admin).with_products(&[product.clone()]); - - context.set_block_timestamp_in_days(365); - - let amount = 5_000_000; - let ticket = JarTicket { - product_id: product.id, - valid_until: U64(100000000), - timezone: None, - }; - - let signature = signer.sign(context.get_signature_material(&alice, &ticket, amount).as_str()); - - context - .contract() - .verify(&alice, amount, &ticket, Some(Base64VecU8(signature))); - } - - #[test] - #[should_panic(expected = "Product 'not_existing_product' doesn't exist")] - fn verify_ticket_with_not_existing_product() { - let admin = admin(); - - let mut context = Context::new(admin.clone()); - - context.switch_account(&admin); - - let signer = MessageSigner::new(); - let not_existing_product = generate_premium_product("not_existing_product", &signer); - - let amount = 500_000; - let ticket = JarTicket { - product_id: not_existing_product.id, - valid_until: U64(100000000), - timezone: None, - }; - - let signature = signer.sign(context.get_signature_material(&admin, &ticket, amount).as_str()); - - context - .contract() - .verify(&admin, amount, &ticket, Some(Base64VecU8(signature))); - } - - #[test] - #[should_panic(expected = "Signature is required")] - fn verify_ticket_without_signature_when_required() { - let admin = admin(); - - let signer = MessageSigner::new(); - let product = generate_premium_product("not_existing_product", &signer); - let context = Context::new(admin.clone()).with_products(&[product.clone()]); - - let amount = 3_000_000; - let ticket = JarTicket { - product_id: product.id, - valid_until: U64(100000000), - timezone: None, - }; - - context.contract().verify(&admin, amount, &ticket, None); - } - - #[test] - fn verify_ticket_without_signature_when_not_required() { - let admin = admin(); - - let product = Product::new(); - let context = Context::new(admin.clone()).with_products(&[product.clone()]); - - let amount = 4_000_000_000; - let ticket = JarTicket { - product_id: product.id, - valid_until: U64(0), - timezone: None, - }; - - context.contract().verify(&admin, amount, &ticket, None); - } - - #[test] - #[should_panic(expected = "It's not possible to create new jars for this product")] - fn create_jar_for_disabled_product() { - let alice = alice(); - let admin = admin(); - - let product = Product::new().enabled(false); - let context = Context::new(admin).with_products(&[product.clone()]); - - let ticket = JarTicket { - product_id: product.id, - valid_until: U64(0), - timezone: None, - }; - context.contract().create_jar(alice, ticket, U128(1_000_000), None); - } -} +// #![cfg(test)] +// +// use fake::Fake; +// use near_sdk::Timestamp; +// use sweat_jar_model::{UDecimal, MS_IN_YEAR}; +// +// use crate::{ +// product::model::{Apy, Product}, +// Jar, +// }; +// +// #[test] +// fn get_interest_before_maturity() { +// let product = Product::new().lockup_term(2 * MS_IN_YEAR); +// let jar = Jar::new(0).principal(100_000_000); +// +// let interest = jar.get_interest(&[], &product, MS_IN_YEAR).0; +// assert_eq!(12_000_000, interest); +// } +// +// #[test] +// fn get_interest_after_maturity() { +// let product = Product::new(); +// let jar = Jar::new(0).principal(100_000_000); +// +// let interest = jar.get_interest(&[], &product, 400 * 24 * 60 * 60 * 1000).0; +// assert_eq!(12_000_000, interest); +// } +// +// #[test] +// fn interest_precision() { +// let product = Product::new().apy(Apy::Constant(UDecimal::new(1, 0))); +// let jar = Jar::new(0).principal(MS_IN_YEAR as u128); +// +// assert_eq!(jar.get_interest(&[], &product, 10000000000).0, 10000000000); +// assert_eq!(jar.get_interest(&[], &product, 10000000001).0, 10000000001); +// +// for _ in 0..100 { +// let time: Timestamp = (10..MS_IN_YEAR).fake(); +// assert_eq!(jar.get_interest(&[], &product, time).0, time as u128); +// } +// } +// +// #[cfg(test)] +// mod signature_tests { +// +// use near_sdk::{ +// json_types::{Base64VecU8, U128, U64}, +// test_utils::test_env::alice, +// }; +// +// use crate::{ +// common::tests::Context, +// jar::model::JarTicket, +// product::{helpers::MessageSigner, model::Product}, +// test_utils::{admin, generate_premium_product}, +// }; +// +// #[test] +// fn verify_ticket_with_valid_signature_and_date() { +// let admin = admin(); +// +// let signer = MessageSigner::new(); +// let product = generate_premium_product("premium_product", &signer); +// let context = Context::new(admin.clone()).with_products(&[product.clone()]); +// +// let amount = 14_000_000; +// let ticket = JarTicket { +// product_id: product.id, +// valid_until: U64(123000000), +// timezone: None, +// }; +// +// let signature = signer.sign(context.get_signature_material(&admin, &ticket, amount).as_str()); +// +// context +// .contract() +// .verify(&admin, amount, &ticket, Some(Base64VecU8(signature))); +// } +// +// #[test] +// #[should_panic(expected = "Signature must be 64 bytes")] +// fn verify_ticket_with_invalid_signature() { +// let alice = alice(); +// let admin = admin(); +// +// let signer = MessageSigner::new(); +// let product = generate_premium_product("premium_product", &signer); +// let context = Context::new(admin).with_products(&[product.clone()]); +// +// let amount = 1_000_000; +// let ticket = JarTicket { +// product_id: product.id, +// valid_until: U64(100000000), +// timezone: None, +// }; +// +// let signature: Vec = vec![0, 1, 2]; +// +// context +// .contract() +// .verify(&alice, amount, &ticket, Some(Base64VecU8(signature))); +// } +// +// #[test] +// #[should_panic(expected = "Not matching signature")] +// fn verify_ticket_with_not_matching_signature() { +// let admin = admin(); +// +// let signer = MessageSigner::new(); +// let product = generate_premium_product("premium_product", &signer); +// let another_product = generate_premium_product("another_premium_product", &MessageSigner::new()); +// +// let context = Context::new(admin.clone()).with_products(&[product, another_product.clone()]); +// +// let amount = 15_000_000; +// let ticket_for_another_product = JarTicket { +// product_id: another_product.id, +// valid_until: U64(100000000), +// timezone: None, +// }; +// +// // signature made for wrong product +// let signature = signer.sign( +// context +// .get_signature_material(&admin, &ticket_for_another_product, amount) +// .as_str(), +// ); +// +// context.contract().verify( +// &admin, +// amount, +// &ticket_for_another_product, +// Some(Base64VecU8(signature)), +// ); +// } +// +// #[test] +// #[should_panic(expected = "Ticket is outdated")] +// fn verify_ticket_with_invalid_date() { +// let alice = alice(); +// let admin = admin(); +// +// let signer = MessageSigner::new(); +// let product = generate_premium_product("premium_product", &signer); +// let mut context = Context::new(admin).with_products(&[product.clone()]); +// +// context.set_block_timestamp_in_days(365); +// +// let amount = 5_000_000; +// let ticket = JarTicket { +// product_id: product.id, +// valid_until: U64(100000000), +// timezone: None, +// }; +// +// let signature = signer.sign(context.get_signature_material(&alice, &ticket, amount).as_str()); +// +// context +// .contract() +// .verify(&alice, amount, &ticket, Some(Base64VecU8(signature))); +// } +// +// #[test] +// #[should_panic(expected = "Product 'not_existing_product' doesn't exist")] +// fn verify_ticket_with_not_existing_product() { +// let admin = admin(); +// +// let mut context = Context::new(admin.clone()); +// +// context.switch_account(&admin); +// +// let signer = MessageSigner::new(); +// let not_existing_product = generate_premium_product("not_existing_product", &signer); +// +// let amount = 500_000; +// let ticket = JarTicket { +// product_id: not_existing_product.id, +// valid_until: U64(100000000), +// timezone: None, +// }; +// +// let signature = signer.sign(context.get_signature_material(&admin, &ticket, amount).as_str()); +// +// context +// .contract() +// .verify(&admin, amount, &ticket, Some(Base64VecU8(signature))); +// } +// +// #[test] +// #[should_panic(expected = "Signature is required")] +// fn verify_ticket_without_signature_when_required() { +// let admin = admin(); +// +// let signer = MessageSigner::new(); +// let product = generate_premium_product("not_existing_product", &signer); +// let context = Context::new(admin.clone()).with_products(&[product.clone()]); +// +// let amount = 3_000_000; +// let ticket = JarTicket { +// product_id: product.id, +// valid_until: U64(100000000), +// timezone: None, +// }; +// +// context.contract().verify(&admin, amount, &ticket, None); +// } +// +// #[test] +// fn verify_ticket_without_signature_when_not_required() { +// let admin = admin(); +// +// let product = Product::new(); +// let context = Context::new(admin.clone()).with_products(&[product.clone()]); +// +// let amount = 4_000_000_000; +// let ticket = JarTicket { +// product_id: product.id, +// valid_until: U64(0), +// timezone: None, +// }; +// +// context.contract().verify(&admin, amount, &ticket, None); +// } +// +// #[test] +// #[should_panic(expected = "It's not possible to create new jars for this product")] +// fn create_jar_for_disabled_product() { +// let alice = alice(); +// let admin = admin(); +// +// let product = Product::new().enabled(false); +// let context = Context::new(admin).with_products(&[product.clone()]); +// +// let ticket = JarTicket { +// product_id: product.id, +// valid_until: U64(0), +// timezone: None, +// }; +// context.contract().create_jar(alice, ticket, U128(1_000_000), None); +// } +// } diff --git a/contract/src/product/helpers.rs b/contract/src/product/helpers.rs index 967fbb62..65264d7a 100644 --- a/contract/src/product/helpers.rs +++ b/contract/src/product/helpers.rs @@ -6,12 +6,15 @@ use ed25519_dalek::{Signer, SigningKey}; use general_purpose::STANDARD; use near_sdk::AccountId; use rand::rngs::OsRng; -use sweat_jar_model::{Score, TokenAmount, UDecimal, MS_IN_YEAR}; +use sweat_jar_model::{TokenAmount, UDecimal, MS_IN_YEAR}; use crate::{ - common::{tests::Context, Duration}, + common::tests::Context, jar::model::JarTicket, - product::model::{Apy, Cap, FixedProductTerms, Product, Terms, WithdrawalFee}, + product::model::{ + v2::{Apy, Cap, DowngradableApy, FixedProductTerms, Terms, WithdrawalFee}, + ProductV2, + }, test_utils::PRODUCT, Contract, }; @@ -43,26 +46,26 @@ impl MessageSigner { } } -impl Product { +impl ProductV2 { pub fn new() -> Self { Self { id: PRODUCT.to_string(), - apy: Apy::Constant(UDecimal::new(12, 2)), cap: Cap { min: 0, max: 1_000_000 }, terms: Terms::Fixed(FixedProductTerms { lockup_term: MS_IN_YEAR, - allows_top_up: false, - allows_restaking: false, + apy: Apy::Downgradable(DowngradableApy { + default: UDecimal::new(20, 2), + fallback: UDecimal::new(10, 2), + }), }), withdrawal_fee: None, public_key: None, is_enabled: true, - score_cap: 0, } } } -impl Product { +impl ProductV2 { pub(crate) fn id(mut self, id: &str) -> Self { self.id = id.to_string(); self @@ -83,68 +86,13 @@ impl Product { self } - pub(crate) fn flexible(mut self) -> Self { - self.terms = Terms::Flexible; - self - } - pub(crate) fn with_withdrawal_fee(mut self, fee: WithdrawalFee) -> Self { self.withdrawal_fee = Some(fee); self } - pub(crate) fn lockup_term(mut self, term: Duration) -> Self { - self.terms = match self.terms { - Terms::Fixed(terms) => Terms::Fixed(FixedProductTerms { - lockup_term: term, - ..terms - }), - Terms::Flexible => Terms::Fixed(FixedProductTerms { - lockup_term: term, - allows_top_up: false, - allows_restaking: false, - }), - }; - - self - } - - pub(crate) fn with_allows_top_up(mut self, allows_top_up: bool) -> Self { - self.terms = match self.terms { - Terms::Fixed(terms) => Terms::Fixed(FixedProductTerms { allows_top_up, ..terms }), - Terms::Flexible => Terms::Fixed(FixedProductTerms { - allows_top_up, - lockup_term: MS_IN_YEAR, - allows_restaking: false, - }), - }; - - self - } - - pub(crate) fn with_allows_restaking(mut self, allows_restaking: bool) -> Self { - self.terms = match self.terms { - Terms::Fixed(terms) => Terms::Fixed(FixedProductTerms { - allows_restaking, - ..terms - }), - Terms::Flexible => Terms::Fixed(FixedProductTerms { - allows_restaking, - lockup_term: MS_IN_YEAR, - allows_top_up: false, - }), - }; - - self - } - - pub(crate) fn apy(mut self, apy: impl Into) -> Self { - self.apy = apy.into(); - self - } - - pub(crate) fn score_cap(mut self, cap: Score) -> Self { - self.score_cap = cap; + pub(crate) fn terms(mut self, terms: Terms) -> Self { + self.terms = terms; self } } diff --git a/contract/src/product/tests.rs b/contract/src/product/tests.rs index dd5bd35a..198ec1cb 100644 --- a/contract/src/product/tests.rs +++ b/contract/src/product/tests.rs @@ -1,282 +1,282 @@ -#![cfg(test)] - -use near_sdk::{ - json_types::{Base64VecU8, U128, U64}, - test_utils::test_env::alice, -}; -use sweat_jar_model::{ - api::ProductApi, - product::{ - ApyView, DowngradableApyView, FixedProductTermsDto, ProductDto, ProductView, TermsDto, TermsView, - WithdrawalFeeDto, WithdrawalFeeView, - }, - UDecimal, MS_IN_YEAR, -}; - -use crate::{ - common::tests::Context, - product::{ - helpers::MessageSigner, - model::{Apy, DowngradableApy, Product, Terms, WithdrawalFee}, - }, - test_utils::admin, -}; - -pub(crate) fn get_register_product_command() -> ProductDto { - ProductDto { - id: "product".to_string(), - ..Default::default() - } -} - -#[test] -fn disable_product_when_enabled() { - let admin = admin(); - let product = &Product::new(); - - let mut context = Context::new(admin.clone()).with_products(&[product.clone()]); - - let mut product = context.contract().get_product(&product.id); - assert!(product.is_enabled); - - context.switch_account(&admin); - context.with_deposit_yocto(1, |context| { - context.contract().set_enabled(product.id.to_string(), false) - }); - - context.contract().products_cache.borrow_mut().clear(); - - product = context.contract().get_product(&product.id); - assert!(!product.is_enabled); -} - -#[test] -#[should_panic(expected = "Status matches")] -fn enable_product_when_enabled() { - let admin = admin(); - let product = &Product::new(); - - let mut context = Context::new(admin.clone()).with_products(&[product.clone()]); - - let product = context.contract().get_product(&product.id); - assert!(product.is_enabled); - - context.switch_account(&admin); - context.with_deposit_yocto(1, |context| { - context.contract().set_enabled(product.id.to_string(), true) - }); -} - -#[test] -#[should_panic(expected = "Product already exists")] -fn register_product_with_existing_id() { - let admin = admin(); - - let mut context = Context::new(admin.clone()); - - context.switch_account(&admin); - - context.with_deposit_yocto(1, |context| { - let first_command = get_register_product_command(); - context.contract().register_product(first_command) - }); - - context.with_deposit_yocto(1, |context| { - let second_command = get_register_product_command(); - context.contract().register_product(second_command) - }); -} - -fn register_product(command: ProductDto) -> (Product, ProductView) { - let admin = admin(); - - let mut context = Context::new(admin.clone()); - - context.switch_account(&admin); - context.with_deposit_yocto(1, |context| context.contract().register_product(command)); - - let product = context.contract().products.into_iter().last().unwrap().1.clone(); - let view = context.contract().get_products().first().unwrap().clone(); - - (product, view) -} - -#[test] -fn register_downgradable_product() { - let (product, view) = register_product(ProductDto { - id: "downgradable_product".to_string(), - apy_fallback: Some((U128(10), 3)), - ..Default::default() - }); - - assert_eq!( - product.apy, - Apy::Downgradable(DowngradableApy { - default: UDecimal { - significand: 12, - exponent: 2 - }, - fallback: UDecimal { - significand: 10, - exponent: 3 - }, - }) - ); - - assert_eq!( - view.apy, - ApyView::Downgradable(DowngradableApyView { - default: 0.12, - fallback: 0.01 - }) - ) -} - -#[test] -#[should_panic( - expected = "Fee for this product is too high. It is possible for customer to pay more in fees than he staked." -)] -fn register_product_with_too_high_fixed_fee() { - register_product(ProductDto { - id: "product_with_fixed_fee".to_string(), - withdrawal_fee: WithdrawalFeeDto::Fix(U128(200)).into(), - terms: TermsDto::Fixed(FixedProductTermsDto { - lockup_term: U64(MS_IN_YEAR), - allows_top_up: false, - allows_restaking: false, - }), - ..Default::default() - }); -} - -#[test] -#[should_panic( - expected = "Fee for this product is too high. It is possible for customer to pay more in fees than he staked." -)] -fn register_product_with_too_high_percent_fee() { - register_product(ProductDto { - id: "product_with_fixed_fee".to_string(), - withdrawal_fee: WithdrawalFeeDto::Percent(U128(100), 0).into(), - ..Default::default() - }); -} - -#[test] -fn register_product_with_fee() { - let (product, view) = register_product(ProductDto { - id: "product_with_fixed_fee".to_string(), - withdrawal_fee: WithdrawalFeeDto::Fix(U128(10)).into(), - ..Default::default() - }); - - assert_eq!(product.withdrawal_fee, Some(WithdrawalFee::Fix(10))); - - assert_eq!(view.withdrawal_fee, Some(WithdrawalFeeView::Fix(U128(10)))); - - let (product, view) = register_product(ProductDto { - id: "product_with_percent_fee".to_string(), - withdrawal_fee: WithdrawalFeeDto::Percent(U128(12), 2).into(), - ..Default::default() - }); - - assert_eq!( - product.withdrawal_fee, - Some(WithdrawalFee::Percent(UDecimal { - significand: 12, - exponent: 2 - })) - ); - - assert_eq!(view.withdrawal_fee, Some(WithdrawalFeeView::Percent(0.12))); -} - -#[test] -fn register_product_with_flexible_terms() { - let (product, view) = register_product(ProductDto { - id: "product_with_fixed_fee".to_string(), - terms: TermsDto::Flexible, - ..Default::default() - }); - - assert_eq!(product.terms, Terms::Flexible); - assert_eq!(view.terms, TermsView::Flexible); -} - -#[test] -fn set_public_key() { - let admin = admin(); - - let signer = MessageSigner::new(); - let product = generate_product().public_key(signer.public_key()); - let mut context = Context::new(admin.clone()).with_products(&[product.clone()]); - - let new_signer = MessageSigner::new(); - let new_pk = new_signer.public_key(); - - context.switch_account(&admin); - context.with_deposit_yocto(1, |context| { - context - .contract() - .set_public_key(product.id.clone(), Base64VecU8(new_pk.clone())) - }); - - let product = context.contract().products.get(&product.id).unwrap(); - assert_eq!(&new_pk, product.public_key.as_ref().unwrap()); -} - -#[test] -#[should_panic(expected = "Can be performed only by admin")] -fn set_public_key_by_not_admin() { - let alice = alice(); - let admin = admin(); - - let signer = MessageSigner::new(); - let product = generate_product().public_key(signer.public_key()); - let mut context = Context::new(admin).with_products(&[product.clone()]); - - let new_signer = MessageSigner::new(); - let new_pk = new_signer.public_key(); - - context.switch_account(&alice); - context.with_deposit_yocto(1, |context| { - context.contract().set_public_key(product.id, Base64VecU8(new_pk)) - }); -} - -#[test] -#[should_panic(expected = "Requires attached deposit of exactly 1 yoctoNEAR")] -fn set_public_key_without_deposit() { - let admin = admin(); - - let signer = MessageSigner::new(); - let product = generate_product().public_key(signer.public_key()); - let mut context = Context::new(admin.clone()).with_products(&[product.clone()]); - - let new_signer = MessageSigner::new(); - let new_pk = new_signer.public_key(); - - context.switch_account(&admin); - - context.contract().set_public_key(product.id, Base64VecU8(new_pk)); -} - -#[test] -fn assert_cap_in_bounds() { - generate_product().assert_cap(200); -} - -#[test] -#[should_panic(expected = "Total amount is out of product bounds: [100..100000000000]")] -fn assert_cap_less_than_min() { - generate_product().assert_cap(10); -} - -#[test] -#[should_panic(expected = "Total amount is out of product bounds: [100..100000000000]")] -fn assert_cap_more_than_max() { - generate_product().assert_cap(500_000_000_000); -} - -fn generate_product() -> Product { - Product::new().cap(100, 100_000_000_000) -} +// #![cfg(test)] +// +// use near_sdk::{ +// json_types::{Base64VecU8, U128, U64}, +// test_utils::test_env::alice, +// }; +// use sweat_jar_model::{ +// api::ProductApi, +// product::{ +// ApyView, DowngradableApyView, FixedProductTermsDto, ProductDto, ProductView, TermsDto, TermsView, +// WithdrawalFeeDto, WithdrawalFeeView, +// }, +// UDecimal, MS_IN_YEAR, +// }; +// +// use crate::{ +// common::tests::Context, +// product::{ +// helpers::MessageSigner, +// model::{Apy, DowngradableApy, Product, Terms, WithdrawalFee}, +// }, +// test_utils::admin, +// }; +// +// pub(crate) fn get_register_product_command() -> ProductDto { +// ProductDto { +// id: "product".to_string(), +// ..Default::default() +// } +// } +// +// #[test] +// fn disable_product_when_enabled() { +// let admin = admin(); +// let product = &Product::new(); +// +// let mut context = Context::new(admin.clone()).with_products(&[product.clone()]); +// +// let mut product = context.contract().get_product(&product.id); +// assert!(product.is_enabled); +// +// context.switch_account(&admin); +// context.with_deposit_yocto(1, |context| { +// context.contract().set_enabled(product.id.to_string(), false) +// }); +// +// context.contract().products_cache.borrow_mut().clear(); +// +// product = context.contract().get_product(&product.id); +// assert!(!product.is_enabled); +// } +// +// #[test] +// #[should_panic(expected = "Status matches")] +// fn enable_product_when_enabled() { +// let admin = admin(); +// let product = &Product::new(); +// +// let mut context = Context::new(admin.clone()).with_products(&[product.clone()]); +// +// let product = context.contract().get_product(&product.id); +// assert!(product.is_enabled); +// +// context.switch_account(&admin); +// context.with_deposit_yocto(1, |context| { +// context.contract().set_enabled(product.id.to_string(), true) +// }); +// } +// +// #[test] +// #[should_panic(expected = "Product already exists")] +// fn register_product_with_existing_id() { +// let admin = admin(); +// +// let mut context = Context::new(admin.clone()); +// +// context.switch_account(&admin); +// +// context.with_deposit_yocto(1, |context| { +// let first_command = get_register_product_command(); +// context.contract().register_product(first_command) +// }); +// +// context.with_deposit_yocto(1, |context| { +// let second_command = get_register_product_command(); +// context.contract().register_product(second_command) +// }); +// } +// +// fn register_product(command: ProductDto) -> (Product, ProductView) { +// let admin = admin(); +// +// let mut context = Context::new(admin.clone()); +// +// context.switch_account(&admin); +// context.with_deposit_yocto(1, |context| context.contract().register_product(command)); +// +// let product = context.contract().products.into_iter().last().unwrap().1.clone(); +// let view = context.contract().get_products().first().unwrap().clone(); +// +// (product, view) +// } +// +// #[test] +// fn register_downgradable_product() { +// let (product, view) = register_product(ProductDto { +// id: "downgradable_product".to_string(), +// apy_fallback: Some((U128(10), 3)), +// ..Default::default() +// }); +// +// assert_eq!( +// product.apy, +// Apy::Downgradable(DowngradableApy { +// default: UDecimal { +// significand: 12, +// exponent: 2 +// }, +// fallback: UDecimal { +// significand: 10, +// exponent: 3 +// }, +// }) +// ); +// +// assert_eq!( +// view.apy, +// ApyView::Downgradable(DowngradableApyView { +// default: 0.12, +// fallback: 0.01 +// }) +// ) +// } +// +// #[test] +// #[should_panic( +// expected = "Fee for this product is too high. It is possible for customer to pay more in fees than he staked." +// )] +// fn register_product_with_too_high_fixed_fee() { +// register_product(ProductDto { +// id: "product_with_fixed_fee".to_string(), +// withdrawal_fee: WithdrawalFeeDto::Fix(U128(200)).into(), +// terms: TermsDto::Fixed(FixedProductTermsDto { +// lockup_term: U64(MS_IN_YEAR), +// allows_top_up: false, +// allows_restaking: false, +// }), +// ..Default::default() +// }); +// } +// +// #[test] +// #[should_panic( +// expected = "Fee for this product is too high. It is possible for customer to pay more in fees than he staked." +// )] +// fn register_product_with_too_high_percent_fee() { +// register_product(ProductDto { +// id: "product_with_fixed_fee".to_string(), +// withdrawal_fee: WithdrawalFeeDto::Percent(U128(100), 0).into(), +// ..Default::default() +// }); +// } +// +// #[test] +// fn register_product_with_fee() { +// let (product, view) = register_product(ProductDto { +// id: "product_with_fixed_fee".to_string(), +// withdrawal_fee: WithdrawalFeeDto::Fix(U128(10)).into(), +// ..Default::default() +// }); +// +// assert_eq!(product.withdrawal_fee, Some(WithdrawalFee::Fix(10))); +// +// assert_eq!(view.withdrawal_fee, Some(WithdrawalFeeView::Fix(U128(10)))); +// +// let (product, view) = register_product(ProductDto { +// id: "product_with_percent_fee".to_string(), +// withdrawal_fee: WithdrawalFeeDto::Percent(U128(12), 2).into(), +// ..Default::default() +// }); +// +// assert_eq!( +// product.withdrawal_fee, +// Some(WithdrawalFee::Percent(UDecimal { +// significand: 12, +// exponent: 2 +// })) +// ); +// +// assert_eq!(view.withdrawal_fee, Some(WithdrawalFeeView::Percent(0.12))); +// } +// +// #[test] +// fn register_product_with_flexible_terms() { +// let (product, view) = register_product(ProductDto { +// id: "product_with_fixed_fee".to_string(), +// terms: TermsDto::Flexible, +// ..Default::default() +// }); +// +// assert_eq!(product.terms, Terms::Flexible); +// assert_eq!(view.terms, TermsView::Flexible); +// } +// +// #[test] +// fn set_public_key() { +// let admin = admin(); +// +// let signer = MessageSigner::new(); +// let product = generate_product().public_key(signer.public_key()); +// let mut context = Context::new(admin.clone()).with_products(&[product.clone()]); +// +// let new_signer = MessageSigner::new(); +// let new_pk = new_signer.public_key(); +// +// context.switch_account(&admin); +// context.with_deposit_yocto(1, |context| { +// context +// .contract() +// .set_public_key(product.id.clone(), Base64VecU8(new_pk.clone())) +// }); +// +// let product = context.contract().products.get(&product.id).unwrap(); +// assert_eq!(&new_pk, product.public_key.as_ref().unwrap()); +// } +// +// #[test] +// #[should_panic(expected = "Can be performed only by admin")] +// fn set_public_key_by_not_admin() { +// let alice = alice(); +// let admin = admin(); +// +// let signer = MessageSigner::new(); +// let product = generate_product().public_key(signer.public_key()); +// let mut context = Context::new(admin).with_products(&[product.clone()]); +// +// let new_signer = MessageSigner::new(); +// let new_pk = new_signer.public_key(); +// +// context.switch_account(&alice); +// context.with_deposit_yocto(1, |context| { +// context.contract().set_public_key(product.id, Base64VecU8(new_pk)) +// }); +// } +// +// #[test] +// #[should_panic(expected = "Requires attached deposit of exactly 1 yoctoNEAR")] +// fn set_public_key_without_deposit() { +// let admin = admin(); +// +// let signer = MessageSigner::new(); +// let product = generate_product().public_key(signer.public_key()); +// let mut context = Context::new(admin.clone()).with_products(&[product.clone()]); +// +// let new_signer = MessageSigner::new(); +// let new_pk = new_signer.public_key(); +// +// context.switch_account(&admin); +// +// context.contract().set_public_key(product.id, Base64VecU8(new_pk)); +// } +// +// #[test] +// fn assert_cap_in_bounds() { +// generate_product().assert_cap(200); +// } +// +// #[test] +// #[should_panic(expected = "Total amount is out of product bounds: [100..100000000000]")] +// fn assert_cap_less_than_min() { +// generate_product().assert_cap(10); +// } +// +// #[test] +// #[should_panic(expected = "Total amount is out of product bounds: [100..100000000000]")] +// fn assert_cap_more_than_max() { +// generate_product().assert_cap(500_000_000_000); +// } +// +// fn generate_product() -> Product { +// Product::new().cap(100, 100_000_000_000) +// } diff --git a/contract/src/score/account_score.rs b/contract/src/score/account_score.rs index e321866c..8082865f 100644 --- a/contract/src/score/account_score.rs +++ b/contract/src/score/account_score.rs @@ -129,90 +129,90 @@ impl Default for AccountScore { } } -#[cfg(test)] -mod test { - use near_sdk::env::block_timestamp_ms; - use sweat_jar_model::{Day, Timezone, MS_IN_DAY, MS_IN_HOUR, UTC}; - - use crate::{ - score::{account_score::Chain, AccountScore}, - test_builder::TestBuilder, - }; - - const TIMEZONE: Timezone = Timezone::hour_shift(3); - const TODAY: u64 = 1722234632000; - - fn generate_chain() -> Chain { - let today: Day = TODAY.into(); - - vec![ - (1_000, today), - (1_000, today - (MS_IN_HOUR * 3).into()), - (1_000, today - (MS_IN_HOUR * 12).into()), - (1_000, today - (MS_IN_HOUR * 25).into()), - (1_000, today - (MS_IN_HOUR * 28).into()), - (1_000, today - (MS_IN_HOUR * 40).into()), - (1_000, today - (MS_IN_HOUR * 45).into()), - (1_000, today - (MS_IN_HOUR * 48).into()), - (1_000, today - (MS_IN_HOUR * 55).into()), - (1_000, today - (MS_IN_HOUR * 550).into()), - ] - } - - #[test] - fn test_account_score() { - let mut ctx = TestBuilder::new().build(); - - ctx.set_block_timestamp_in_ms(TODAY); - - let product = Product::new().score_cap(20_000); - - let mut account_score = AccountScore::new(TIMEZONE); - - account_score.update(generate_chain()); - - assert_eq!(product.apy_for_score(&account_score.claimable_score()).to_f32(), 0.03); - - ctx.advance_block_timestamp_days(1); - assert_eq!(product.apy_for_score(&account_score.claimable_score()).to_f32(), 0.05); - - ctx.advance_block_timestamp_days(1); - assert_eq!(product.apy_for_score(&account_score.claimable_score()).to_f32(), 0.05); - - assert_eq!(account_score.claim_score(), vec![2000, 3000]); - - assert_eq!(product.apy_for_score(&account_score.claimable_score()).to_f32(), 0.00); - } - - #[test] - #[should_panic(expected = "Walk data from future")] - fn steps_from_future() { - let mut ctx = TestBuilder::new().build(); - ctx.set_block_timestamp_today(); - - let mut account_score = AccountScore::new(TIMEZONE); - account_score.update(vec![(1_000, (block_timestamp_ms() + MS_IN_DAY).into())]); - } - - #[test] - fn updated_on_different_days() { - let mut score = AccountScore { - updated: UTC(MS_IN_DAY * 10), - timezone: Timezone::hour_shift(0), - scores: [1000, 2000], - }; - - let mut ctx = TestBuilder::new().build(); - - ctx.set_block_timestamp_in_ms(MS_IN_DAY * 10); - - score.update(vec![(6, (MS_IN_DAY * 10).into()), (5, (MS_IN_DAY * 9).into())]); - - assert_eq!(score.updated, (MS_IN_DAY * 10).into()); - assert_eq!(score.scores(), (1006, 2005)); - assert_eq!(score.claim_score(), vec![2005]); - - ctx.set_block_timestamp_in_ms(MS_IN_DAY * 11); - assert_eq!(score.claim_score(), vec![1006, 0]); - } -} +// #[cfg(test)] +// mod test { +// use near_sdk::env::block_timestamp_ms; +// use sweat_jar_model::{Day, Timezone, MS_IN_DAY, MS_IN_HOUR, UTC}; +// +// use crate::{ +// score::{account_score::Chain, AccountScore}, +// test_builder::TestBuilder, +// }; +// +// const TIMEZONE: Timezone = Timezone::hour_shift(3); +// const TODAY: u64 = 1722234632000; +// +// fn generate_chain() -> Chain { +// let today: Day = TODAY.into(); +// +// vec![ +// (1_000, today), +// (1_000, today - (MS_IN_HOUR * 3).into()), +// (1_000, today - (MS_IN_HOUR * 12).into()), +// (1_000, today - (MS_IN_HOUR * 25).into()), +// (1_000, today - (MS_IN_HOUR * 28).into()), +// (1_000, today - (MS_IN_HOUR * 40).into()), +// (1_000, today - (MS_IN_HOUR * 45).into()), +// (1_000, today - (MS_IN_HOUR * 48).into()), +// (1_000, today - (MS_IN_HOUR * 55).into()), +// (1_000, today - (MS_IN_HOUR * 550).into()), +// ] +// } +// +// #[test] +// fn test_account_score() { +// let mut ctx = TestBuilder::new().build(); +// +// ctx.set_block_timestamp_in_ms(TODAY); +// +// let product = Product::new().score_cap(20_000); +// +// let mut account_score = AccountScore::new(TIMEZONE); +// +// account_score.update(generate_chain()); +// +// assert_eq!(product.apy_for_score(&account_score.claimable_score()).to_f32(), 0.03); +// +// ctx.advance_block_timestamp_days(1); +// assert_eq!(product.apy_for_score(&account_score.claimable_score()).to_f32(), 0.05); +// +// ctx.advance_block_timestamp_days(1); +// assert_eq!(product.apy_for_score(&account_score.claimable_score()).to_f32(), 0.05); +// +// assert_eq!(account_score.claim_score(), vec![2000, 3000]); +// +// assert_eq!(product.apy_for_score(&account_score.claimable_score()).to_f32(), 0.00); +// } +// +// #[test] +// #[should_panic(expected = "Walk data from future")] +// fn steps_from_future() { +// let mut ctx = TestBuilder::new().build(); +// ctx.set_block_timestamp_today(); +// +// let mut account_score = AccountScore::new(TIMEZONE); +// account_score.update(vec![(1_000, (block_timestamp_ms() + MS_IN_DAY).into())]); +// } +// +// #[test] +// fn updated_on_different_days() { +// let mut score = AccountScore { +// updated: UTC(MS_IN_DAY * 10), +// timezone: Timezone::hour_shift(0), +// scores: [1000, 2000], +// }; +// +// let mut ctx = TestBuilder::new().build(); +// +// ctx.set_block_timestamp_in_ms(MS_IN_DAY * 10); +// +// score.update(vec![(6, (MS_IN_DAY * 10).into()), (5, (MS_IN_DAY * 9).into())]); +// +// assert_eq!(score.updated, (MS_IN_DAY * 10).into()); +// assert_eq!(score.scores(), (1006, 2005)); +// assert_eq!(score.claim_score(), vec![2005]); +// +// ctx.set_block_timestamp_in_ms(MS_IN_DAY * 11); +// assert_eq!(score.claim_score(), vec![1006, 0]); +// } +// } diff --git a/contract/src/score/charts.rs b/contract/src/score/charts.rs index 2ce54079..db8e852c 100644 --- a/contract/src/score/charts.rs +++ b/contract/src/score/charts.rs @@ -1,170 +1,170 @@ -#![cfg(test)] - -use anyhow::Result; -use fake::Fake; -use itertools::Itertools; -use near_sdk::test_utils::test_env::{alice, bob}; -use sweat_jar_model::{jar::JarId, Score, Timezone, MS_IN_DAY, UTC}; -use visu::{render_chart, Graph}; - -use crate::{ - common::test_data::set_test_log_events, - test_builder::{JarField, ProductField::*, TestAccess, TestBuilder}, - test_utils::{admin, PRODUCT, SCORE_PRODUCT}, -}; - -fn generate_year_data() -> (Vec, Vec) { - const JAR: JarId = 0; - const STEP_JAR: JarId = 1; - - set_test_log_events(false); - - let mut ctx = TestBuilder::new() - .product(SCORE_PRODUCT, [APY(0), ScoreCap(20_000)]) - .jar(STEP_JAR, JarField::Timezone(Timezone::hour_shift(3))) - .product(PRODUCT, APY(12)) - .jar(JAR, ()) - .build(); - - let mut result = vec![]; - - ctx.switch_account(admin()); - - for day in 1..400 { - ctx.set_block_timestamp_in_days(day.try_into().unwrap()); - - if day < 100 { - ctx.record_score(UTC(MS_IN_DAY * day), (4_000..10_000).fake(), alice()); - } else { - ctx.record_score(UTC(MS_IN_DAY * day), (15_000..20_000).fake(), alice()); - } - - result.push((ctx.interest(STEP_JAR), ctx.interest(JAR))); - } - - result.into_iter().unzip() -} - -#[test] -#[ignore] -fn plot_year() -> Result<()> { - let (score, simple) = generate_year_data(); - - render_chart(Graph { - title: "Step Jars Interest", - data: [&score, &simple], - legend: ["Step Jar", "Simple Jar"], - x_title: "Days", - y_title: "Interest", - output_file: "../docs/year_walk.png", - ..Default::default() - })?; - - Ok(()) -} - -fn generate_first_week_data() -> (Vec, Vec, Vec, Vec, Vec) { - const ALICE_JAR: JarId = 0; - const BOB_JAR: JarId = 1; - - set_test_log_events(false); - - let mut ctx = TestBuilder::new() - .product(SCORE_PRODUCT, [APY(0), ScoreCap(20_000)]) - .jar(ALICE_JAR, JarField::Timezone(Timezone::hour_shift(0))) - .jar( - BOB_JAR, - [JarField::Account(bob()), JarField::Timezone(Timezone::hour_shift(0))], - ) - .build(); - - let mut result = vec![]; - let mut score_walked: u128; - - let mut total_claimed: u128 = 0; - - for hour in 0..(24 * 5) { - let day = hour / 24; - - ctx.set_block_timestamp_in_hours(hour); - - let score: Score = (0..1000).fake(); - - ctx.switch_account(admin()); - ctx.record_score(UTC(day * MS_IN_DAY), score, alice()); - ctx.record_score(UTC(day * MS_IN_DAY), score, bob()); - - if day > 1 { - ctx.record_score(UTC((day - 1) * MS_IN_DAY), score, alice()); - ctx.record_score(UTC((day - 1) * MS_IN_DAY), score, bob()); - } - - score_walked = u128::from(score); - - let (today, yesterday) = ctx.score(ALICE_JAR).scores(); - - // if hour % 15 == 0 { - let claimed = ctx.claim_total(bob()); - total_claimed += claimed; - // } - - result.push(( - score_walked, - ctx.interest(ALICE_JAR), - total_claimed, - today as u128, - yesterday as u128, - )); - } - - let (today, yesterday) = ctx.score(BOB_JAR).scores(); - - let claimed = ctx.claim_total(bob()); - total_claimed += claimed; - - result.push(( - 0, - ctx.interest(ALICE_JAR), - total_claimed, - today as u128, - yesterday as u128, - )); - - result.into_iter().multiunzip() -} - -#[test] -#[ignore] -fn plot_first_week() -> Result<()> { - let (score_walked, interest_alice, claimed, today, yesterday) = generate_first_week_data(); - - render_chart(Graph { - title: "Step Jars First Week", - data: [&score_walked, &interest_alice, &today, &yesterday, &claimed], - legend: ["Steps Walked", "Interest Alice", "Today", "Yesterday", "Claimed"], - x_title: "Hours", - y_title: "Interest", - output_file: "../docs/first_week.png", - ..Default::default() - })?; - - Ok(()) -} - -#[test] -#[ignore] -fn plot_first_week_with_claim() -> Result<()> { - // let (score_walked, ideal_jar, real_jar, claimed_ideal, claimed_real) = generate_first_week_data(true); - // - // render_chart(Graph { - // title: "Step Jars First Week With Claim", - // data: [&score_walked, &ideal_jar, &real_jar, &claimed_ideal, &claimed_real], - // legend: ["Steps Walked", "Ideal jar", "Real Jar", "Claimed Ideal", "Claimed Real"], - // x_title: "Hours", - // y_title: "Interest", - // output_file: "../docs/first_week_claim.png", - // ..Default::default() - // })?; - - Ok(()) -} +// #![cfg(test)] +// +// use anyhow::Result; +// use fake::Fake; +// use itertools::Itertools; +// use near_sdk::test_utils::test_env::{alice, bob}; +// use sweat_jar_model::{jar::JarId, Score, Timezone, MS_IN_DAY, UTC}; +// use visu::{render_chart, Graph}; +// +// use crate::{ +// common::test_data::set_test_log_events, +// test_builder::{JarField, ProductField::*, TestAccess, TestBuilder}, +// test_utils::{admin, PRODUCT, SCORE_PRODUCT}, +// }; +// +// fn generate_year_data() -> (Vec, Vec) { +// const JAR: JarId = 0; +// const STEP_JAR: JarId = 1; +// +// set_test_log_events(false); +// +// let mut ctx = TestBuilder::new() +// .product(SCORE_PRODUCT, [APY(0), ScoreCap(20_000)]) +// .jar(STEP_JAR, JarField::Timezone(Timezone::hour_shift(3))) +// .product(PRODUCT, APY(12)) +// .jar(JAR, ()) +// .build(); +// +// let mut result = vec![]; +// +// ctx.switch_account(admin()); +// +// for day in 1..400 { +// ctx.set_block_timestamp_in_days(day.try_into().unwrap()); +// +// if day < 100 { +// ctx.record_score(UTC(MS_IN_DAY * day), (4_000..10_000).fake(), alice()); +// } else { +// ctx.record_score(UTC(MS_IN_DAY * day), (15_000..20_000).fake(), alice()); +// } +// +// result.push((ctx.interest(STEP_JAR), ctx.interest(JAR))); +// } +// +// result.into_iter().unzip() +// } +// +// #[test] +// #[ignore] +// fn plot_year() -> Result<()> { +// let (score, simple) = generate_year_data(); +// +// render_chart(Graph { +// title: "Step Jars Interest", +// data: [&score, &simple], +// legend: ["Step Jar", "Simple Jar"], +// x_title: "Days", +// y_title: "Interest", +// output_file: "../docs/year_walk.png", +// ..Default::default() +// })?; +// +// Ok(()) +// } +// +// fn generate_first_week_data() -> (Vec, Vec, Vec, Vec, Vec) { +// const ALICE_JAR: JarId = 0; +// const BOB_JAR: JarId = 1; +// +// set_test_log_events(false); +// +// let mut ctx = TestBuilder::new() +// .product(SCORE_PRODUCT, [APY(0), ScoreCap(20_000)]) +// .jar(ALICE_JAR, JarField::Timezone(Timezone::hour_shift(0))) +// .jar( +// BOB_JAR, +// [JarField::Account(bob()), JarField::Timezone(Timezone::hour_shift(0))], +// ) +// .build(); +// +// let mut result = vec![]; +// let mut score_walked: u128; +// +// let mut total_claimed: u128 = 0; +// +// for hour in 0..(24 * 5) { +// let day = hour / 24; +// +// ctx.set_block_timestamp_in_hours(hour); +// +// let score: Score = (0..1000).fake(); +// +// ctx.switch_account(admin()); +// ctx.record_score(UTC(day * MS_IN_DAY), score, alice()); +// ctx.record_score(UTC(day * MS_IN_DAY), score, bob()); +// +// if day > 1 { +// ctx.record_score(UTC((day - 1) * MS_IN_DAY), score, alice()); +// ctx.record_score(UTC((day - 1) * MS_IN_DAY), score, bob()); +// } +// +// score_walked = u128::from(score); +// +// let (today, yesterday) = ctx.score(ALICE_JAR).scores(); +// +// // if hour % 15 == 0 { +// let claimed = ctx.claim_total(bob()); +// total_claimed += claimed; +// // } +// +// result.push(( +// score_walked, +// ctx.interest(ALICE_JAR), +// total_claimed, +// today as u128, +// yesterday as u128, +// )); +// } +// +// let (today, yesterday) = ctx.score(BOB_JAR).scores(); +// +// let claimed = ctx.claim_total(bob()); +// total_claimed += claimed; +// +// result.push(( +// 0, +// ctx.interest(ALICE_JAR), +// total_claimed, +// today as u128, +// yesterday as u128, +// )); +// +// result.into_iter().multiunzip() +// } +// +// #[test] +// #[ignore] +// fn plot_first_week() -> Result<()> { +// let (score_walked, interest_alice, claimed, today, yesterday) = generate_first_week_data(); +// +// render_chart(Graph { +// title: "Step Jars First Week", +// data: [&score_walked, &interest_alice, &today, &yesterday, &claimed], +// legend: ["Steps Walked", "Interest Alice", "Today", "Yesterday", "Claimed"], +// x_title: "Hours", +// y_title: "Interest", +// output_file: "../docs/first_week.png", +// ..Default::default() +// })?; +// +// Ok(()) +// } +// +// #[test] +// #[ignore] +// fn plot_first_week_with_claim() -> Result<()> { +// // let (score_walked, ideal_jar, real_jar, claimed_ideal, claimed_real) = generate_first_week_data(true); +// // +// // render_chart(Graph { +// // title: "Step Jars First Week With Claim", +// // data: [&score_walked, &ideal_jar, &real_jar, &claimed_ideal, &claimed_real], +// // legend: ["Steps Walked", "Ideal jar", "Real Jar", "Claimed Ideal", "Claimed Real"], +// // x_title: "Hours", +// // y_title: "Interest", +// // output_file: "../docs/first_week_claim.png", +// // ..Default::default() +// // })?; +// +// Ok(()) +// } diff --git a/contract/src/score/tests.rs b/contract/src/score/tests.rs index e8c65279..baeb23e4 100644 --- a/contract/src/score/tests.rs +++ b/contract/src/score/tests.rs @@ -1,311 +1,311 @@ -#![cfg(test)] - -use fake::Fake; -use near_sdk::{ - store::LookupMap, - test_utils::test_env::{alice, bob}, - NearToken, -}; -use sweat_jar_model::{ - api::{ProductApi, ScoreApi, WithdrawApi}, - jar::JarId, - product::ProductDto, - Score, Timezone, MS_IN_DAY, UTC, -}; - -use crate::{ - common::{ - test_data::{set_test_future_success, set_test_log_events}, - tests::Context, - }, - test_builder::{JarField, ProductField::*, TestAccess, TestBuilder}, - test_utils::{admin, expect_panic, UnwrapPromise, PRODUCT, SCORE_PRODUCT}, - StorageKey, -}; - -#[test] -#[should_panic(expected = "Can be performed only by admin")] -fn record_score_by_non_manager() { - let ctx = TestBuilder::new().build(); - ctx.contract().record_score(vec![(alice(), vec![(100, 0.into())])]); -} - -#[test] -fn create_invalid_step_product() { - let mut ctx = TestBuilder::new().build(); - - let mut command = ProductDto { - id: "aa".to_string(), - apy_default: (10.into(), 3), - apy_fallback: None, - cap_min: Default::default(), - cap_max: Default::default(), - terms: Default::default(), - withdrawal_fee: None, - public_key: None, - is_enabled: false, - score_cap: 1000, - }; - - ctx.switch_account(admin()); - - ctx.set_deposit_yocto(1); - - expect_panic(&ctx, "Step based products do not support constant APY", || { - ctx.contract().register_product(command.clone()); - }); - - command.apy_fallback = Some((10.into(), 3)); - - expect_panic(&ctx, "Step based products do not support downgradable APY", || { - ctx.contract().register_product(command); - }); -} - -/// 12% jar should have the same interest as 12_000 score jar walking to the limit every day -/// Also this method tests score cap -#[test] -fn same_interest_in_score_jar_as_in_const_jar() { - const JAR: JarId = 0; - const SCORE_JAR: JarId = 1; - - const DAYS: u64 = 400; - const HALF_PERIOD: u64 = DAYS / 2; - - set_test_log_events(false); - - let mut ctx = TestBuilder::new() - .product(PRODUCT, APY(12)) - .jar(JAR, ()) - .product(SCORE_PRODUCT, [APY(0), ScoreCap(12_000)]) - .jar(SCORE_JAR, JarField::Timezone(Timezone::hour_shift(3))) - .build(); - - // Difference of 1 is okay because the missing yoctosweat is stored in claim remainder - // and will eventually be added to total claimed balance - fn compare_interest(ctx: &Context) { - let diff = ctx.interest(JAR) as i128 - ctx.interest(SCORE_JAR) as i128; - assert!(diff <= 1, "Diff is too big {diff}"); - } - - let mut total_claimed = 0; - - for day in 0..DAYS { - ctx.set_block_timestamp_in_days(day); - - ctx.switch_account(admin()); - ctx.record_score(UTC(day * MS_IN_DAY), 20_000, alice()); - - compare_interest(&ctx); - - if day == HALF_PERIOD { - let jar_interest = ctx.interest(JAR); - let score_interest = ctx.interest(SCORE_JAR); - - let claimed = ctx.claim_total(alice()); - - total_claimed += claimed; - - assert_eq!(claimed, jar_interest + score_interest); - } - } - - assert_eq!(ctx.jar(JAR).cache.unwrap().updated_at, HALF_PERIOD * MS_IN_DAY); - assert_eq!(ctx.jar(SCORE_JAR).cache.unwrap().updated_at, (DAYS - 1) * MS_IN_DAY); - - compare_interest(&ctx); - - total_claimed += ctx.claim_total(alice()); - - assert_eq!(total_claimed, NearToken::from_near(24).as_yoctonear()); -} - -#[test] -fn score_jar_claim_often_vs_claim_at_the_end() { - const ALICE_JAR: JarId = 0; - const BOB_JAR: JarId = 1; - - set_test_log_events(false); - - let mut ctx = TestBuilder::new() - .product(SCORE_PRODUCT, [APY(0), ScoreCap(20_000)]) - .jar(ALICE_JAR, JarField::Timezone(Timezone::hour_shift(0))) - .jar( - BOB_JAR, - [JarField::Account(bob()), JarField::Timezone(Timezone::hour_shift(0))], - ) - .build(); - - fn update_and_check(day: u64, ctx: &mut Context, total_claimed_bob: &mut u128) { - let score: Score = (0..1000).fake(); - - ctx.switch_account(admin()); - ctx.record_score(UTC(day * MS_IN_DAY), score, alice()); - ctx.record_score(UTC(day * MS_IN_DAY), score, bob()); - - if day > 1 { - ctx.switch_account(admin()); - ctx.record_score(UTC((day - 1) * MS_IN_DAY), score, alice()); - ctx.record_score(UTC((day - 1) * MS_IN_DAY), score, bob()); - } - - *total_claimed_bob += ctx.claim_total(bob()); - assert_eq!(ctx.interest(ALICE_JAR), *total_claimed_bob, "{day}"); - } - - let mut total_claimed_bob: u128 = 0; - - // Update each hour for 10 days - for hour in 0..(24 * 10) { - ctx.set_block_timestamp_in_hours(hour); - update_and_check(hour / 24, &mut ctx, &mut total_claimed_bob); - } - - // Update each day until 100 days has passed - for day in 10..100 { - ctx.set_block_timestamp_in_days(day); - update_and_check(day, &mut ctx, &mut total_claimed_bob); - } - - total_claimed_bob += ctx.claim_total(bob()); - - assert_eq!(ctx.interest(ALICE_JAR), total_claimed_bob); - assert_eq!(ctx.claim_total(alice()), total_claimed_bob); - - assert_eq!(ctx.jar(ALICE_JAR).cache.unwrap().updated_at, MS_IN_DAY * 99); -} - -#[test] -fn interest_does_not_increase_with_no_steps() { - const ALICE_JAR: JarId = 0; - - set_test_log_events(false); - - let mut ctx = TestBuilder::new() - .product(SCORE_PRODUCT, [APY(0), ScoreCap(20_000)]) - .jar(ALICE_JAR, JarField::Timezone(Timezone::hour_shift(0))) - .build(); - - ctx.set_block_timestamp_in_days(5); - - ctx.record_score(UTC(5 * MS_IN_DAY), 1000, alice()); - - assert_eq!(ctx.interest(ALICE_JAR), 0); - - ctx.set_block_timestamp_in_days(6); - - let interest_for_one_day = ctx.interest(ALICE_JAR); - assert_ne!(interest_for_one_day, 0); - - ctx.set_block_timestamp_in_days(7); - assert_eq!(interest_for_one_day, ctx.interest(ALICE_JAR)); - - ctx.set_block_timestamp_in_days(50); - assert_eq!(interest_for_one_day, ctx.interest(ALICE_JAR)); - - ctx.set_block_timestamp_in_days(100); - assert_eq!(interest_for_one_day, ctx.interest(ALICE_JAR)); -} - -#[test] -fn withdraw_score_jar() { - const ALICE_JAR: JarId = 0; - const BOB_JAR: JarId = 1; - - set_test_log_events(false); - - let mut ctx = TestBuilder::new() - .product(SCORE_PRODUCT, [APY(0), TermDays(7), ScoreCap(20_000)]) - .jar(ALICE_JAR, JarField::Timezone(Timezone::hour_shift(0))) - .jar( - BOB_JAR, - [JarField::Account(bob()), JarField::Timezone(Timezone::hour_shift(0))], - ) - .build(); - - for i in 0..=10 { - ctx.set_block_timestamp_in_days(i); - - ctx.record_score((i * MS_IN_DAY).into(), 1000, alice()); - ctx.record_score((i * MS_IN_DAY).into(), 1000, bob()); - - if i == 5 { - let claimed_alice = ctx.claim_total(alice()); - let claimed_bob = ctx.claim_total(bob()); - assert_eq!(claimed_alice, claimed_bob); - } - } - - // Alice claims first and then withdraws - ctx.switch_account(alice()); - let claimed_alice = ctx.claim_total(alice()); - let withdrawn_alice = ctx - .contract() - .withdraw(ALICE_JAR.into(), None) - .unwrap() - .withdrawn_amount - .0; - - assert_eq!(ctx.claim_total(alice()), 0); - - // Bob withdraws first and then claims - ctx.switch_account(bob()); - let withdrawn_bob = ctx - .contract() - .withdraw(BOB_JAR.into(), None) - .unwrap() - .withdrawn_amount - .0; - let claimed_bob = ctx.claim_total(bob()); - - assert_eq!(ctx.claim_total(bob()), 0); - - assert_eq!(claimed_alice, claimed_bob); - assert_eq!(withdrawn_alice, withdrawn_bob); - - // All jars were closed and deleted after full withdraw and claim - assert!(ctx.contract().account_jars(&alice()).is_empty()); - assert!(ctx.contract().account_jars(&bob()).is_empty()); -} - -#[test] -fn revert_scores_on_failed_claim() { - const ALICE_JAR: JarId = 0; - - set_test_log_events(false); - - let mut ctx = TestBuilder::new() - .product(SCORE_PRODUCT, [APY(0), TermDays(10), ScoreCap(20_000)]) - .jar(ALICE_JAR, JarField::Timezone(Timezone::hour_shift(0))) - .build(); - - for day in 0..=10 { - ctx.set_block_timestamp_in_days(day); - - ctx.record_score((day * MS_IN_DAY).into(), 500, alice()); - if day > 1 { - ctx.record_score(((day - 1) * MS_IN_DAY).into(), 1000, alice()); - } - - // Clear accounts cache to test deserialization - if day == 3 { - ctx.contract().accounts.flush(); - ctx.contract().accounts = LookupMap::new(StorageKey::AccountsVersioned); - } - - // Normal claim. Score should change: - if day == 4 { - assert_eq!(ctx.score(ALICE_JAR).scores(), (500, 1000)); - assert_ne!(ctx.claim_total(alice()), 0); - assert_eq!(ctx.score(ALICE_JAR).scores(), (500, 0)); - } - - // Failed claim. Score should stay the same: - if day == 8 { - set_test_future_success(false); - assert_eq!(ctx.score(ALICE_JAR).scores(), (500, 1000)); - assert_eq!(ctx.claim_total(alice()), 0); - assert_eq!(ctx.score(ALICE_JAR).scores(), (500, 1000)); - } - } -} +// #![cfg(test)] +// +// use fake::Fake; +// use near_sdk::{ +// store::LookupMap, +// test_utils::test_env::{alice, bob}, +// NearToken, +// }; +// use sweat_jar_model::{ +// api::{ProductApi, ScoreApi, WithdrawApi}, +// jar::JarId, +// product::ProductDto, +// Score, Timezone, MS_IN_DAY, UTC, +// }; +// +// use crate::{ +// common::{ +// test_data::{set_test_future_success, set_test_log_events}, +// tests::Context, +// }, +// test_builder::{JarField, ProductField::*, TestAccess, TestBuilder}, +// test_utils::{admin, expect_panic, UnwrapPromise, PRODUCT, SCORE_PRODUCT}, +// StorageKey, +// }; +// +// #[test] +// #[should_panic(expected = "Can be performed only by admin")] +// fn record_score_by_non_manager() { +// let ctx = TestBuilder::new().build(); +// ctx.contract().record_score(vec![(alice(), vec![(100, 0.into())])]); +// } +// +// #[test] +// fn create_invalid_step_product() { +// let mut ctx = TestBuilder::new().build(); +// +// let mut command = ProductDto { +// id: "aa".to_string(), +// apy_default: (10.into(), 3), +// apy_fallback: None, +// cap_min: Default::default(), +// cap_max: Default::default(), +// terms: Default::default(), +// withdrawal_fee: None, +// public_key: None, +// is_enabled: false, +// score_cap: 1000, +// }; +// +// ctx.switch_account(admin()); +// +// ctx.set_deposit_yocto(1); +// +// expect_panic(&ctx, "Step based products do not support constant APY", || { +// ctx.contract().register_product(command.clone()); +// }); +// +// command.apy_fallback = Some((10.into(), 3)); +// +// expect_panic(&ctx, "Step based products do not support downgradable APY", || { +// ctx.contract().register_product(command); +// }); +// } +// +// /// 12% jar should have the same interest as 12_000 score jar walking to the limit every day +// /// Also this method tests score cap +// #[test] +// fn same_interest_in_score_jar_as_in_const_jar() { +// const JAR: JarId = 0; +// const SCORE_JAR: JarId = 1; +// +// const DAYS: u64 = 400; +// const HALF_PERIOD: u64 = DAYS / 2; +// +// set_test_log_events(false); +// +// let mut ctx = TestBuilder::new() +// .product(PRODUCT, APY(12)) +// .jar(JAR, ()) +// .product(SCORE_PRODUCT, [APY(0), ScoreCap(12_000)]) +// .jar(SCORE_JAR, JarField::Timezone(Timezone::hour_shift(3))) +// .build(); +// +// // Difference of 1 is okay because the missing yoctosweat is stored in claim remainder +// // and will eventually be added to total claimed balance +// fn compare_interest(ctx: &Context) { +// let diff = ctx.interest(JAR) as i128 - ctx.interest(SCORE_JAR) as i128; +// assert!(diff <= 1, "Diff is too big {diff}"); +// } +// +// let mut total_claimed = 0; +// +// for day in 0..DAYS { +// ctx.set_block_timestamp_in_days(day); +// +// ctx.switch_account(admin()); +// ctx.record_score(UTC(day * MS_IN_DAY), 20_000, alice()); +// +// compare_interest(&ctx); +// +// if day == HALF_PERIOD { +// let jar_interest = ctx.interest(JAR); +// let score_interest = ctx.interest(SCORE_JAR); +// +// let claimed = ctx.claim_total(alice()); +// +// total_claimed += claimed; +// +// assert_eq!(claimed, jar_interest + score_interest); +// } +// } +// +// assert_eq!(ctx.jar(JAR).cache.unwrap().updated_at, HALF_PERIOD * MS_IN_DAY); +// assert_eq!(ctx.jar(SCORE_JAR).cache.unwrap().updated_at, (DAYS - 1) * MS_IN_DAY); +// +// compare_interest(&ctx); +// +// total_claimed += ctx.claim_total(alice()); +// +// assert_eq!(total_claimed, NearToken::from_near(24).as_yoctonear()); +// } +// +// #[test] +// fn score_jar_claim_often_vs_claim_at_the_end() { +// const ALICE_JAR: JarId = 0; +// const BOB_JAR: JarId = 1; +// +// set_test_log_events(false); +// +// let mut ctx = TestBuilder::new() +// .product(SCORE_PRODUCT, [APY(0), ScoreCap(20_000)]) +// .jar(ALICE_JAR, JarField::Timezone(Timezone::hour_shift(0))) +// .jar( +// BOB_JAR, +// [JarField::Account(bob()), JarField::Timezone(Timezone::hour_shift(0))], +// ) +// .build(); +// +// fn update_and_check(day: u64, ctx: &mut Context, total_claimed_bob: &mut u128) { +// let score: Score = (0..1000).fake(); +// +// ctx.switch_account(admin()); +// ctx.record_score(UTC(day * MS_IN_DAY), score, alice()); +// ctx.record_score(UTC(day * MS_IN_DAY), score, bob()); +// +// if day > 1 { +// ctx.switch_account(admin()); +// ctx.record_score(UTC((day - 1) * MS_IN_DAY), score, alice()); +// ctx.record_score(UTC((day - 1) * MS_IN_DAY), score, bob()); +// } +// +// *total_claimed_bob += ctx.claim_total(bob()); +// assert_eq!(ctx.interest(ALICE_JAR), *total_claimed_bob, "{day}"); +// } +// +// let mut total_claimed_bob: u128 = 0; +// +// // Update each hour for 10 days +// for hour in 0..(24 * 10) { +// ctx.set_block_timestamp_in_hours(hour); +// update_and_check(hour / 24, &mut ctx, &mut total_claimed_bob); +// } +// +// // Update each day until 100 days has passed +// for day in 10..100 { +// ctx.set_block_timestamp_in_days(day); +// update_and_check(day, &mut ctx, &mut total_claimed_bob); +// } +// +// total_claimed_bob += ctx.claim_total(bob()); +// +// assert_eq!(ctx.interest(ALICE_JAR), total_claimed_bob); +// assert_eq!(ctx.claim_total(alice()), total_claimed_bob); +// +// assert_eq!(ctx.jar(ALICE_JAR).cache.unwrap().updated_at, MS_IN_DAY * 99); +// } +// +// #[test] +// fn interest_does_not_increase_with_no_steps() { +// const ALICE_JAR: JarId = 0; +// +// set_test_log_events(false); +// +// let mut ctx = TestBuilder::new() +// .product(SCORE_PRODUCT, [APY(0), ScoreCap(20_000)]) +// .jar(ALICE_JAR, JarField::Timezone(Timezone::hour_shift(0))) +// .build(); +// +// ctx.set_block_timestamp_in_days(5); +// +// ctx.record_score(UTC(5 * MS_IN_DAY), 1000, alice()); +// +// assert_eq!(ctx.interest(ALICE_JAR), 0); +// +// ctx.set_block_timestamp_in_days(6); +// +// let interest_for_one_day = ctx.interest(ALICE_JAR); +// assert_ne!(interest_for_one_day, 0); +// +// ctx.set_block_timestamp_in_days(7); +// assert_eq!(interest_for_one_day, ctx.interest(ALICE_JAR)); +// +// ctx.set_block_timestamp_in_days(50); +// assert_eq!(interest_for_one_day, ctx.interest(ALICE_JAR)); +// +// ctx.set_block_timestamp_in_days(100); +// assert_eq!(interest_for_one_day, ctx.interest(ALICE_JAR)); +// } +// +// #[test] +// fn withdraw_score_jar() { +// const ALICE_JAR: JarId = 0; +// const BOB_JAR: JarId = 1; +// +// set_test_log_events(false); +// +// let mut ctx = TestBuilder::new() +// .product(SCORE_PRODUCT, [APY(0), TermDays(7), ScoreCap(20_000)]) +// .jar(ALICE_JAR, JarField::Timezone(Timezone::hour_shift(0))) +// .jar( +// BOB_JAR, +// [JarField::Account(bob()), JarField::Timezone(Timezone::hour_shift(0))], +// ) +// .build(); +// +// for i in 0..=10 { +// ctx.set_block_timestamp_in_days(i); +// +// ctx.record_score((i * MS_IN_DAY).into(), 1000, alice()); +// ctx.record_score((i * MS_IN_DAY).into(), 1000, bob()); +// +// if i == 5 { +// let claimed_alice = ctx.claim_total(alice()); +// let claimed_bob = ctx.claim_total(bob()); +// assert_eq!(claimed_alice, claimed_bob); +// } +// } +// +// // Alice claims first and then withdraws +// ctx.switch_account(alice()); +// let claimed_alice = ctx.claim_total(alice()); +// let withdrawn_alice = ctx +// .contract() +// .withdraw(ALICE_JAR.into(), None) +// .unwrap() +// .withdrawn_amount +// .0; +// +// assert_eq!(ctx.claim_total(alice()), 0); +// +// // Bob withdraws first and then claims +// ctx.switch_account(bob()); +// let withdrawn_bob = ctx +// .contract() +// .withdraw(BOB_JAR.into(), None) +// .unwrap() +// .withdrawn_amount +// .0; +// let claimed_bob = ctx.claim_total(bob()); +// +// assert_eq!(ctx.claim_total(bob()), 0); +// +// assert_eq!(claimed_alice, claimed_bob); +// assert_eq!(withdrawn_alice, withdrawn_bob); +// +// // All jars were closed and deleted after full withdraw and claim +// assert!(ctx.contract().account_jars(&alice()).is_empty()); +// assert!(ctx.contract().account_jars(&bob()).is_empty()); +// } +// +// #[test] +// fn revert_scores_on_failed_claim() { +// const ALICE_JAR: JarId = 0; +// +// set_test_log_events(false); +// +// let mut ctx = TestBuilder::new() +// .product(SCORE_PRODUCT, [APY(0), TermDays(10), ScoreCap(20_000)]) +// .jar(ALICE_JAR, JarField::Timezone(Timezone::hour_shift(0))) +// .build(); +// +// for day in 0..=10 { +// ctx.set_block_timestamp_in_days(day); +// +// ctx.record_score((day * MS_IN_DAY).into(), 500, alice()); +// if day > 1 { +// ctx.record_score(((day - 1) * MS_IN_DAY).into(), 1000, alice()); +// } +// +// // Clear accounts cache to test deserialization +// if day == 3 { +// ctx.contract().accounts.flush(); +// ctx.contract().accounts = LookupMap::new(StorageKey::AccountsVersioned); +// } +// +// // Normal claim. Score should change: +// if day == 4 { +// assert_eq!(ctx.score(ALICE_JAR).scores(), (500, 1000)); +// assert_ne!(ctx.claim_total(alice()), 0); +// assert_eq!(ctx.score(ALICE_JAR).scores(), (500, 0)); +// } +// +// // Failed claim. Score should stay the same: +// if day == 8 { +// set_test_future_success(false); +// assert_eq!(ctx.score(ALICE_JAR).scores(), (500, 1000)); +// assert_eq!(ctx.claim_total(alice()), 0); +// assert_eq!(ctx.score(ALICE_JAR).scores(), (500, 1000)); +// } +// } +// } diff --git a/contract/src/test_builder/product_builder.rs b/contract/src/test_builder/product_builder.rs index d6f81269..7921e1f7 100644 --- a/contract/src/test_builder/product_builder.rs +++ b/contract/src/test_builder/product_builder.rs @@ -1,37 +1,37 @@ -use sweat_jar_model::{Score, MS_IN_DAY}; - -use crate::product::model::Product; - -pub(crate) trait ProductBuilder: Sized { - fn apply(self, product: Product) -> Product; - fn build(self, id: &'static str) -> Product { - let product = Product::new().id(id); - self.apply(product) - } -} - -pub(crate) enum ProductField { - APY(u32), - ScoreCap(Score), - TermDays(u64), -} - -impl ProductBuilder for ProductField { - fn apply(self, product: Product) -> Product { - match self { - ProductField::APY(apy) => product.apy(apy), - ProductField::ScoreCap(cap) => product.score_cap(cap), - ProductField::TermDays(days) => product.lockup_term(days * MS_IN_DAY), - } - } -} - -impl ProductBuilder for [ProductField; SIZE] { - fn apply(self, product: Product) -> Product { - let mut product = product; - for p in self { - product = p.apply(product) - } - product - } -} +// use sweat_jar_model::{Score, MS_IN_DAY}; +// +// use crate::product::model::Product; +// +// pub(crate) trait ProductBuilder: Sized { +// fn apply(self, product: Product) -> Product; +// fn build(self, id: &'static str) -> Product { +// let product = Product::new().id(id); +// self.apply(product) +// } +// } +// +// pub(crate) enum ProductField { +// APY(u32), +// ScoreCap(Score), +// TermDays(u64), +// } +// +// impl ProductBuilder for ProductField { +// fn apply(self, product: Product) -> Product { +// match self { +// ProductField::APY(apy) => product.apy(apy), +// ProductField::ScoreCap(cap) => product.score_cap(cap), +// ProductField::TermDays(days) => product.lockup_term(days * MS_IN_DAY), +// } +// } +// } +// +// impl ProductBuilder for [ProductField; SIZE] { +// fn apply(self, product: Product) -> Product { +// let mut product = product; +// for p in self { +// product = p.apply(product) +// } +// product +// } +// } diff --git a/contract/src/test_builder/test_access.rs b/contract/src/test_builder/test_access.rs index ec545d55..cc997efe 100644 --- a/contract/src/test_builder/test_access.rs +++ b/contract/src/test_builder/test_access.rs @@ -1,66 +1,66 @@ -use near_sdk::AccountId; -use sweat_jar_model::{ - api::{ClaimApi, JarApi, ScoreApi}, - jar::JarId, - Score, UTC, -}; - -use crate::{ - common::tests::Context, - jar::model::Jar, - product::model::Product, - score::AccountScore, - test_utils::{admin, UnwrapPromise}, -}; - -pub trait TestAccess { - fn _product(&self, id: &str) -> Product; - fn interest(&self, id: JarId) -> u128; - fn record_score(&mut self, timestamp: UTC, score: Score, account_id: AccountId); - fn claim_total(&mut self, account_id: AccountId) -> u128; - fn jar(&self, id: JarId) -> Jar; - fn jar_account_for_id(&self, id: JarId) -> AccountId; - fn score(&self, id: JarId) -> AccountScore; -} - -impl TestAccess for Context { - fn _product(&self, id: &str) -> Product { - self.contract().get_product(&id.to_string()) - } - - fn interest(&self, id: JarId) -> u128 { - let account_id = self.jar_account_for_id(id); - self.contract().get_interest(vec![id.into()], account_id).amount.total.0 - } - - fn record_score(&mut self, timestamp: UTC, score: Score, account_id: AccountId) { - self.switch_account(admin()); - self.contract() - .record_score(vec![(account_id, vec![(score, timestamp)])]) - } - - fn claim_total(&mut self, account_id: AccountId) -> u128 { - self.switch_account(account_id); - self.contract().claim_total(None).unwrap().get_total().0 - } - - fn jar(&self, id: JarId) -> Jar { - let account_id = self.jar_account_for_id(id); - self.contract().get_jar_internal(&account_id, id) - } - - fn jar_account_for_id(&self, id: JarId) -> AccountId { - for (account, jars) in &self.account_jars { - if jars.contains(&id) { - return account.clone(); - } - } - - panic!("Account for jar id: {id} not found") - } - - fn score(&self, id: JarId) -> AccountScore { - let account_id = self.jar_account_for_id(id); - *self.contract().get_score(&account_id).expect("No account score") - } -} +// use near_sdk::AccountId; +// use sweat_jar_model::{ +// api::{ClaimApi, JarApi, ScoreApi}, +// jar::JarId, +// Score, UTC, +// }; +// +// use crate::{ +// common::tests::Context, +// jar::model::Jar, +// product::model::Product, +// score::AccountScore, +// test_utils::{admin, UnwrapPromise}, +// }; +// +// pub trait TestAccess { +// fn _product(&self, id: &str) -> Product; +// fn interest(&self, id: JarId) -> u128; +// fn record_score(&mut self, timestamp: UTC, score: Score, account_id: AccountId); +// fn claim_total(&mut self, account_id: AccountId) -> u128; +// fn jar(&self, id: JarId) -> Jar; +// fn jar_account_for_id(&self, id: JarId) -> AccountId; +// fn score(&self, id: JarId) -> AccountScore; +// } +// +// impl TestAccess for Context { +// fn _product(&self, id: &str) -> Product { +// self.contract().get_product(&id.to_string()) +// } +// +// fn interest(&self, id: JarId) -> u128 { +// let account_id = self.jar_account_for_id(id); +// self.contract().get_interest(vec![id.into()], account_id).amount.total.0 +// } +// +// fn record_score(&mut self, timestamp: UTC, score: Score, account_id: AccountId) { +// self.switch_account(admin()); +// self.contract() +// .record_score(vec![(account_id, vec![(score, timestamp)])]) +// } +// +// fn claim_total(&mut self, account_id: AccountId) -> u128 { +// self.switch_account(account_id); +// self.contract().claim_total(None).unwrap().get_total().0 +// } +// +// fn jar(&self, id: JarId) -> Jar { +// let account_id = self.jar_account_for_id(id); +// self.contract().get_jar_internal(&account_id, id) +// } +// +// fn jar_account_for_id(&self, id: JarId) -> AccountId { +// for (account, jars) in &self.account_jars { +// if jars.contains(&id) { +// return account.clone(); +// } +// } +// +// panic!("Account for jar id: {id} not found") +// } +// +// fn score(&self, id: JarId) -> AccountScore { +// let account_id = self.jar_account_for_id(id); +// *self.contract().get_score(&account_id).expect("No account score") +// } +// } diff --git a/contract/src/test_builder/test_builder.rs b/contract/src/test_builder/test_builder.rs index dc5822b7..7e74ca1b 100644 --- a/contract/src/test_builder/test_builder.rs +++ b/contract/src/test_builder/test_builder.rs @@ -1,67 +1,67 @@ -#![cfg(test)] - -use sweat_jar_model::jar::JarId; - -use crate::{ - common::tests::Context, - jar::model::Jar, - product::model::Product, - score::AccountScore, - test_builder::{jar_builder::JarBuilder, ProductBuilder}, - test_utils::admin, -}; - -pub(crate) struct TestBuilder { - context: Context, - products: Vec, - jars: Vec, -} - -impl TestBuilder { - pub fn new() -> Self { - Self { - context: Context::new(admin()), - products: vec![], - jars: vec![], - } - } -} - -impl TestBuilder { - /// Build and add custom product - pub fn product(mut self, id: &'static str, builder: impl ProductBuilder) -> Self { - self.products.push(builder.build(id)); - self - } - - /// Build and add custom jar - pub fn jar(mut self, id: JarId, builder: impl JarBuilder) -> Self { - let product = self.products.last().expect("Create product first"); - let product_id = &product.id; - - let jar = builder.build(id, product_id, 100); - - let account_id = &jar.account_id; - - if product.is_score_product() { - if self.context.contract().get_score(account_id).is_none() { - let Some(timezone) = builder.timezone() else { - panic!("Step jar without timezone"); - }; - - self.context.contract().accounts.entry(account_id.clone()).or_default(); - - self.context.contract().accounts.get_mut(account_id).unwrap().score = AccountScore::new(timezone); - } - } - - self.jars.push(jar); - self - } -} - -impl TestBuilder { - pub fn build(self) -> Context { - self.context.with_products(&self.products).with_jars(&self.jars) - } -} +// #![cfg(test)] +// +// use sweat_jar_model::jar::JarId; +// +// use crate::{ +// common::tests::Context, +// jar::model::Jar, +// product::model::Product, +// score::AccountScore, +// test_builder::{jar_builder::JarBuilder, ProductBuilder}, +// test_utils::admin, +// }; +// +// pub(crate) struct TestBuilder { +// context: Context, +// products: Vec, +// jars: Vec, +// } +// +// impl TestBuilder { +// pub fn new() -> Self { +// Self { +// context: Context::new(admin()), +// products: vec![], +// jars: vec![], +// } +// } +// } +// +// impl TestBuilder { +// /// Build and add custom product +// pub fn product(mut self, id: &'static str, builder: impl ProductBuilder) -> Self { +// self.products.push(builder.build(id)); +// self +// } +// +// /// Build and add custom jar +// pub fn jar(mut self, id: JarId, builder: impl JarBuilder) -> Self { +// let product = self.products.last().expect("Create product first"); +// let product_id = &product.id; +// +// let jar = builder.build(id, product_id, 100); +// +// let account_id = &jar.account_id; +// +// if product.is_score_product() { +// if self.context.contract().get_score(account_id).is_none() { +// let Some(timezone) = builder.timezone() else { +// panic!("Step jar without timezone"); +// }; +// +// self.context.contract().accounts.entry(account_id.clone()).or_default(); +// +// self.context.contract().accounts.get_mut(account_id).unwrap().score = AccountScore::new(timezone); +// } +// } +// +// self.jars.push(jar); +// self +// } +// } +// +// impl TestBuilder { +// pub fn build(self) -> Context { +// self.context.with_products(&self.products).with_jars(&self.jars) +// } +// } diff --git a/contract/src/test_utils.rs b/contract/src/test_utils.rs index 09049059..b4d22780 100644 --- a/contract/src/test_utils.rs +++ b/contract/src/test_utils.rs @@ -3,12 +3,18 @@ use std::panic::{catch_unwind, UnwindSafe}; use near_sdk::{test_utils::test_env::alice, AccountId, PromiseOrValue}; -use sweat_jar_model::{TokenAmount, UDecimal}; +use sweat_jar_model::{TokenAmount, UDecimal, MS_IN_YEAR}; use crate::{ common::Timestamp, jar::model::{Jar, JarLastVersion}, - product::helpers::MessageSigner, + product::{ + helpers::MessageSigner, + model::{ + v2::{Apy, DowngradableApy, FixedProductTerms, Terms}, + ProductV2, + }, + }, }; pub const PRINCIPAL: u128 = 1_000_000; @@ -64,14 +70,17 @@ impl Jar { } } -pub fn generate_premium_product(id: &str, signer: &MessageSigner) -> Product { - Product::new() +pub fn generate_premium_product(id: &str, signer: &MessageSigner) -> ProductV2 { + ProductV2::new() .id(id) .public_key(signer.public_key()) .cap(0, 100_000_000_000) - .apy(Apy::Downgradable(DowngradableApy { - default: UDecimal::new(20, 2), - fallback: UDecimal::new(10, 2), + .terms(Terms::Fixed(FixedProductTerms { + apy: Apy::Downgradable(DowngradableApy { + default: UDecimal::new(20, 2), + fallback: UDecimal::new(10, 2), + }), + lockup_term: MS_IN_YEAR, })) } diff --git a/contract/src/tests.rs b/contract/src/tests.rs index 913e3353..ebdef532 100644 --- a/contract/src/tests.rs +++ b/contract/src/tests.rs @@ -1,489 +1,489 @@ -#![cfg(test)] - -use std::collections::HashMap; - -use common::tests::Context; -use fake::Fake; -use near_sdk::{ - json_types::U128, - serde_json::{from_str, to_string}, - test_utils::test_env::{alice, bob}, -}; -use sweat_jar_model::{ - api::{ClaimApi, JarApi, PenaltyApi, ProductApi, WithdrawApi}, - jar::{AggregatedTokenAmountView, JarView}, - product::ApyView, - TokenAmount, UDecimal, U32, -}; - -use super::*; -use crate::{ - common::test_data::set_test_log_events, - product::{helpers::MessageSigner, model::DowngradableApy, tests::get_register_product_command}, - test_utils::{admin, UnwrapPromise}, -}; - -#[test] -fn add_product_to_list_by_admin() { - let admin = admin(); - let mut context = Context::new(admin.clone()); - - context.switch_account(&admin); - context.with_deposit_yocto(1, |context| { - context.contract().register_product(get_register_product_command()) - }); - - let products = context.contract().get_products(); - assert_eq!(products.len(), 1); - assert_eq!(products.first().unwrap().id, "product".to_string()); -} - -#[test] -#[should_panic(expected = "Can be performed only by admin")] -fn add_product_to_list_by_not_admin() { - let admin = admin(); - let mut context = Context::new(admin); - - context.with_deposit_yocto(1, |context| { - context.contract().register_product(get_register_product_command()) - }); -} - -#[test] -fn get_principle_with_no_jars() { - let alice = alice(); - let admin = admin(); - let context = Context::new(admin); - - let principal = context.contract().get_total_principal(alice); - assert_eq!(principal.total.0, 0); -} - -#[test] -fn get_principal_with_single_jar() { - let alice = alice(); - let admin = admin(); - - let product = Product::new(); - let reference_jar = Jar::new(0).principal(100); - let context = Context::new(admin) - .with_products(&[product]) - .with_jars(&[reference_jar]); - - let principal = context.contract().get_total_principal(alice).total.0; - assert_eq!(principal, 100); -} - -#[test] -fn get_principal_with_multiple_jars() { - let alice = alice(); - let admin = admin(); - - let product = Product::new(); - let jars = &[ - Jar::new(0).principal(100), - Jar::new(1).principal(200), - Jar::new(2).principal(400), - ]; - - let context = Context::new(admin).with_products(&[product]).with_jars(jars); - - let principal = context.contract().get_total_principal(alice).total.0; - assert_eq!(principal, 700); -} - -#[test] -fn get_total_interest_with_no_jars() { - let alice = alice(); - let admin = admin(); - - let context = Context::new(admin); - - let interest = context.contract().get_total_interest(alice); - - assert_eq!(interest.amount.total.0, 0); - assert_eq!(interest.amount.detailed, HashMap::new()); -} - -#[test] -fn get_total_interest_with_single_jar_after_30_minutes() { - let alice = alice(); - let admin = admin(); - - let product = Product::new(); - - let jar_id = 0; - let jar = Jar::new(jar_id).principal(100_000_000); - let mut context = Context::new(admin).with_products(&[product]).with_jars(&[jar.clone()]); - - let contract_jar = JarView::from(context.contract().accounts.get(&alice).unwrap().get_jar(jar_id)); - assert_eq!(JarView::from(jar), contract_jar); - - context.set_block_timestamp_in_minutes(30); - - let interest = context.contract().get_total_interest(alice); - - assert_eq!(interest.amount.total.0, 684); - assert_eq!(interest.amount.detailed, HashMap::from([(U32(0), U128(684))])) -} - -#[test] -fn get_total_interest_with_single_jar_on_maturity() { - let alice = alice(); - let admin = admin(); - - let product = Product::new(); - - let jar_id = 0; - let jar = Jar::new(jar_id).principal(100_000_000); - let mut context = Context::new(admin).with_products(&[product]).with_jars(&[jar.clone()]); - - let contract_jar = JarView::from(context.contract().accounts.get(&alice).unwrap().get_jar(jar_id)); - assert_eq!(JarView::from(jar), contract_jar); - - context.set_block_timestamp_in_days(365); - - let interest = context.contract().get_total_interest(alice); - - assert_eq!( - interest.amount, - AggregatedTokenAmountView { - detailed: [(U32(0), U128(12_000_000))].into(), - total: U128(12_000_000) - } - ) -} - -#[test] -fn get_total_interest_with_single_jar_after_maturity() { - let alice = alice(); - let admin = admin(); - - let product = Product::new(); - - let jar_id = 0; - let jar = Jar::new(jar_id).principal(100_000_000); - let mut context = Context::new(admin).with_products(&[product]).with_jars(&[jar.clone()]); - - let contract_jar = JarView::from(context.contract().accounts.get(&alice).unwrap().get_jar(jar_id)); - assert_eq!(JarView::from(jar), contract_jar); - - context.set_block_timestamp_in_days(400); - - let interest = context.contract().get_total_interest(alice).amount.total.0; - assert_eq!(interest, 12_000_000); -} - -#[test] -fn get_total_interest_with_single_jar_after_claim_on_half_term_and_maturity() { - let alice = alice(); - let admin = admin(); - - let product = Product::new(); - - let jar_id = 0; - let jar = Jar::new(jar_id).principal(100_000_000); - let mut context = Context::new(admin).with_products(&[product]).with_jars(&[jar.clone()]); - - let contract_jar = JarView::from(context.contract().accounts.get(&alice).unwrap().get_jar(jar_id)); - assert_eq!(JarView::from(jar), contract_jar); - - context.set_block_timestamp_in_days(182); - - let mut interest = context.contract().get_total_interest(alice.clone()).amount.total.0; - assert_eq!(interest, 5_983_561); - - context.switch_account(&alice); - context.contract().claim_total(None); - - context.set_block_timestamp_in_days(365); - - interest = context.contract().get_total_interest(alice.clone()).amount.total.0; - assert_eq!(interest, 6_016_439); -} - -#[test] -#[should_panic(expected = "Penalty is not applicable for constant APY")] -fn penalty_is_not_applicable_for_constant_apy() { - let alice = alice(); - let admin = admin(); - - let signer = MessageSigner::new(); - let product = Product::new() - .apy(Apy::Constant(UDecimal::new(20, 2))) - .public_key(signer.public_key()); - let reference_jar = Jar::new(0).principal(100_000_000); - - let mut context = Context::new(admin.clone()) - .with_products(&[product]) - .with_jars(&[reference_jar]); - - context.switch_account(&admin); - context.contract().set_penalty(alice, U32(0), true); -} - -#[test] -fn get_total_interest_for_premium_with_penalty_after_half_term() { - let alice = alice(); - let admin = admin(); - - let signer = MessageSigner::new(); - let product = Product::new() - .apy(Apy::Downgradable(DowngradableApy { - default: UDecimal::new(20, 2), - fallback: UDecimal::new(10, 2), - })) - .public_key(signer.public_key()); - let reference_jar = Jar::new(0).principal(100_000_000); - - let mut context = Context::new(admin.clone()) - .with_products(&[product]) - .with_jars(&[reference_jar]); - - context.set_block_timestamp_in_ms(15_768_000_000); - - let mut interest = context.contract().get_total_interest(alice.clone()).amount.total.0; - assert_eq!(interest, 10_000_000); - - context.switch_account(&admin); - context.contract().set_penalty(alice.clone(), U32(0), true); - - context.set_block_timestamp_in_ms(31_536_000_000); - - interest = context.contract().get_total_interest(alice).amount.total.0; - assert_eq!(interest, 15_000_000); -} - -#[test] -fn get_total_interest_for_premium_with_multiple_penalties_applied() { - let alice = alice(); - let admin = admin(); - - let signer = MessageSigner::new(); - let product = Product::new() - .apy(Apy::Downgradable(DowngradableApy { - default: UDecimal::new(23, 2), - fallback: UDecimal::new(10, 2), - })) - .lockup_term(3_600_000) - .public_key(signer.public_key()); - let reference_jar = Jar::new(0).principal(100_000_000_000_000_000_000_000); - - let mut context = Context::new(admin.clone()) - .with_products(&[product]) - .with_jars(&[reference_jar]); - - let products = context.contract().get_products(); - assert!(matches!(products.first().unwrap().apy, ApyView::Downgradable(_))); - - context.switch_account(&admin); - - context.set_block_timestamp_in_ms(270_000); - context.contract().set_penalty(alice.clone(), U32(0), true); - - context.set_block_timestamp_in_ms(390_000); - context.contract().set_penalty(alice.clone(), U32(0), false); - - context.set_block_timestamp_in_ms(1_264_000); - context.contract().set_penalty(alice.clone(), U32(0), true); - - context.set_block_timestamp_in_ms(3_700_000); - - let interest = context.contract().get_total_interest(alice.clone()).amount.total.0; - assert_eq!(interest, 1_613_140_537_798_072_044); -} - -#[test] -fn apply_penalty_in_batch() { - let admin = admin(); - let alice = alice(); - let bob = bob(); - - let signer = MessageSigner::new(); - let product = Product::new() - .apy(Apy::Downgradable(DowngradableApy { - default: UDecimal::new(20, 2), - fallback: UDecimal::new(10, 2), - })) - .public_key(signer.public_key()); - - let alice_jars = (0..100).map(|id| Jar::new(id).principal(100_000_000)); - let bob_jars = (0..50).map(|id| Jar::new(id + 200).account_id(&bob).principal(100_000_000)); - - let mut context = Context::new(admin.clone()) - .with_products(&[product]) - .with_jars(&alice_jars.chain(bob_jars).collect::>()); - - context.set_block_timestamp_in_days(182); - - let interest = context.contract().get_total_interest(alice.clone()).amount.total.0; - assert_eq!(interest, 997_260_200); - - let interest = context.contract().get_total_interest(bob.clone()).amount.total.0; - assert_eq!(interest, 498_630_100); - - context.switch_account(&admin); - - let alice_jars = context - .contract() - .get_jars_for_account(alice.clone()) - .into_iter() - .map(|j| j.id) - .collect(); - let bob_jars = context - .contract() - .get_jars_for_account(bob.clone()) - .into_iter() - .map(|j| j.id) - .collect(); - - context - .contract() - .batch_set_penalty(vec![(alice.clone(), alice_jars), (bob.clone(), bob_jars)], true); - - context.set_block_timestamp_in_days(365); - - let interest = context.contract().get_total_interest(alice.clone()).amount.total.0; - assert_eq!(interest, 1_498_630_100); - - let interest = context.contract().get_total_interest(bob.clone()).amount.total.0; - assert_eq!(interest, 749_315_050); - - let alice_jars = context.contract().get_jars_for_account(alice); - let bob_jars = context.contract().get_jars_for_account(bob); - - assert!(alice_jars.into_iter().chain(bob_jars).all(|jar| jar.is_penalty_applied)); -} - -#[test] -fn get_interest_after_withdraw() { - let alice = alice(); - let admin = admin(); - - let product = Product::new(); - let jar = Jar::new(0).principal(100_000_000); - - let mut context = Context::new(admin).with_products(&[product]).with_jars(&[jar.clone()]); - - context.set_block_timestamp_in_days(400); - - context.switch_account(&alice); - context.contract().withdraw(U32(jar.id), None); - - let interest = context.contract().get_total_interest(alice.clone()); - assert_eq!(12_000_000, interest.amount.total.0); -} - -#[test] -#[should_panic(expected = "Can be performed only by admin")] -fn unlock_not_by_manager() { - let alice = alice(); - let admin = admin(); - - let reference_product = Product::new(); - - let mut reference_jar = Jar::new(0).product_id(&reference_product.id).principal(100); - reference_jar.is_pending_withdraw = true; - let jars = &[reference_jar]; - - let mut context = Context::new(admin).with_products(&[reference_product]).with_jars(jars); - - context.switch_account(&alice); - context.contract().unlock_jars_for_account(alice); -} - -#[test] -fn unlock_by_manager() { - let alice = alice(); - let admin = admin(); - - let reference_product = Product::new(); - - let reference_jar_id = 0; - let mut reference_jar = Jar::new(0).product_id(&reference_product.id).principal(100); - reference_jar.is_pending_withdraw = true; - let jars = &[reference_jar]; - - let mut context = Context::new(admin.clone()) - .with_products(&[reference_product]) - .with_jars(jars); - - assert!( - context - .contract() - .get_jar(alice.clone(), reference_jar_id.into()) - .is_pending_withdraw - ); - - context.switch_account(&admin); - context.contract().unlock_jars_for_account(alice.clone()); - - assert!( - !context - .contract() - .get_jar(alice.clone(), reference_jar_id.into()) - .is_pending_withdraw - ); -} - -#[test] -fn test_u32() { - let n = U32(12345678); - - assert_eq!(n, from_str(&to_string(&n).unwrap()).unwrap()); - assert_eq!(U32::from(12345678_u32), n); -} - -#[test] -fn claim_often_vs_claim_once() { - fn test(mut product: Product, principal: TokenAmount, days: u64, n: usize) { - set_test_log_events(false); - - let alice: AccountId = format!("alice_{principal}_{days}_{n}").try_into().unwrap(); - let bob: AccountId = format!("bob_{principal}_{days}_{n}").try_into().unwrap(); - let admin: AccountId = format!("admin_{principal}_{days}_{n}").try_into().unwrap(); - - product.id = format!("product_{principal}_{days}_{n}"); - - let alice_jar = Jar::new(0) - .product_id(&product.id) - .account_id(&alice) - .principal(principal); - - let bob_jar = Jar::new(1) - .product_id(&product.id) - .account_id(&bob) - .principal(principal); - - let mut context = Context::new(admin) - .with_products(&[product]) - .with_jars(&[alice_jar.clone(), bob_jar.clone()]); - - let mut bobs_claimed = 0; - - context.switch_account(&bob); - - for day in 0..days { - context.set_block_timestamp_in_days(day); - let claimed = context.contract().claim_total(None).unwrap(); - bobs_claimed += claimed.get_total().0; - } - - let alice_interest = context.contract().get_total_interest(alice.clone()).amount.total.0; - - assert_eq!(alice_interest, bobs_claimed); - } - - let product = Product::new(); - - test(product.clone(), 10_000_000_000_000_000_000_000_000_000, 365, 0); - - for n in 1..10 { - test( - product.clone(), - (1..10_000_000_000_000_000_000_000_000_000).fake(), - (1..365).fake(), - n, - ); - } -} +// #![cfg(test)] +// +// use std::collections::HashMap; +// +// use common::tests::Context; +// use fake::Fake; +// use near_sdk::{ +// json_types::U128, +// serde_json::{from_str, to_string}, +// test_utils::test_env::{alice, bob}, +// }; +// use sweat_jar_model::{ +// api::{ClaimApi, JarApi, PenaltyApi, ProductApi, WithdrawApi}, +// jar::{AggregatedTokenAmountView, JarView}, +// product::ApyView, +// TokenAmount, UDecimal, U32, +// }; +// +// use super::*; +// use crate::{ +// common::test_data::set_test_log_events, +// product::{helpers::MessageSigner, model::DowngradableApy, tests::get_register_product_command}, +// test_utils::{admin, UnwrapPromise}, +// }; +// +// #[test] +// fn add_product_to_list_by_admin() { +// let admin = admin(); +// let mut context = Context::new(admin.clone()); +// +// context.switch_account(&admin); +// context.with_deposit_yocto(1, |context| { +// context.contract().register_product(get_register_product_command()) +// }); +// +// let products = context.contract().get_products(); +// assert_eq!(products.len(), 1); +// assert_eq!(products.first().unwrap().id, "product".to_string()); +// } +// +// #[test] +// #[should_panic(expected = "Can be performed only by admin")] +// fn add_product_to_list_by_not_admin() { +// let admin = admin(); +// let mut context = Context::new(admin); +// +// context.with_deposit_yocto(1, |context| { +// context.contract().register_product(get_register_product_command()) +// }); +// } +// +// #[test] +// fn get_principle_with_no_jars() { +// let alice = alice(); +// let admin = admin(); +// let context = Context::new(admin); +// +// let principal = context.contract().get_total_principal(alice); +// assert_eq!(principal.total.0, 0); +// } +// +// #[test] +// fn get_principal_with_single_jar() { +// let alice = alice(); +// let admin = admin(); +// +// let product = Product::new(); +// let reference_jar = Jar::new(0).principal(100); +// let context = Context::new(admin) +// .with_products(&[product]) +// .with_jars(&[reference_jar]); +// +// let principal = context.contract().get_total_principal(alice).total.0; +// assert_eq!(principal, 100); +// } +// +// #[test] +// fn get_principal_with_multiple_jars() { +// let alice = alice(); +// let admin = admin(); +// +// let product = Product::new(); +// let jars = &[ +// Jar::new(0).principal(100), +// Jar::new(1).principal(200), +// Jar::new(2).principal(400), +// ]; +// +// let context = Context::new(admin).with_products(&[product]).with_jars(jars); +// +// let principal = context.contract().get_total_principal(alice).total.0; +// assert_eq!(principal, 700); +// } +// +// #[test] +// fn get_total_interest_with_no_jars() { +// let alice = alice(); +// let admin = admin(); +// +// let context = Context::new(admin); +// +// let interest = context.contract().get_total_interest(alice); +// +// assert_eq!(interest.amount.total.0, 0); +// assert_eq!(interest.amount.detailed, HashMap::new()); +// } +// +// #[test] +// fn get_total_interest_with_single_jar_after_30_minutes() { +// let alice = alice(); +// let admin = admin(); +// +// let product = Product::new(); +// +// let jar_id = 0; +// let jar = Jar::new(jar_id).principal(100_000_000); +// let mut context = Context::new(admin).with_products(&[product]).with_jars(&[jar.clone()]); +// +// let contract_jar = JarView::from(context.contract().accounts.get(&alice).unwrap().get_jar(jar_id)); +// assert_eq!(JarView::from(jar), contract_jar); +// +// context.set_block_timestamp_in_minutes(30); +// +// let interest = context.contract().get_total_interest(alice); +// +// assert_eq!(interest.amount.total.0, 684); +// assert_eq!(interest.amount.detailed, HashMap::from([(U32(0), U128(684))])) +// } +// +// #[test] +// fn get_total_interest_with_single_jar_on_maturity() { +// let alice = alice(); +// let admin = admin(); +// +// let product = Product::new(); +// +// let jar_id = 0; +// let jar = Jar::new(jar_id).principal(100_000_000); +// let mut context = Context::new(admin).with_products(&[product]).with_jars(&[jar.clone()]); +// +// let contract_jar = JarView::from(context.contract().accounts.get(&alice).unwrap().get_jar(jar_id)); +// assert_eq!(JarView::from(jar), contract_jar); +// +// context.set_block_timestamp_in_days(365); +// +// let interest = context.contract().get_total_interest(alice); +// +// assert_eq!( +// interest.amount, +// AggregatedTokenAmountView { +// detailed: [(U32(0), U128(12_000_000))].into(), +// total: U128(12_000_000) +// } +// ) +// } +// +// #[test] +// fn get_total_interest_with_single_jar_after_maturity() { +// let alice = alice(); +// let admin = admin(); +// +// let product = Product::new(); +// +// let jar_id = 0; +// let jar = Jar::new(jar_id).principal(100_000_000); +// let mut context = Context::new(admin).with_products(&[product]).with_jars(&[jar.clone()]); +// +// let contract_jar = JarView::from(context.contract().accounts.get(&alice).unwrap().get_jar(jar_id)); +// assert_eq!(JarView::from(jar), contract_jar); +// +// context.set_block_timestamp_in_days(400); +// +// let interest = context.contract().get_total_interest(alice).amount.total.0; +// assert_eq!(interest, 12_000_000); +// } +// +// #[test] +// fn get_total_interest_with_single_jar_after_claim_on_half_term_and_maturity() { +// let alice = alice(); +// let admin = admin(); +// +// let product = Product::new(); +// +// let jar_id = 0; +// let jar = Jar::new(jar_id).principal(100_000_000); +// let mut context = Context::new(admin).with_products(&[product]).with_jars(&[jar.clone()]); +// +// let contract_jar = JarView::from(context.contract().accounts.get(&alice).unwrap().get_jar(jar_id)); +// assert_eq!(JarView::from(jar), contract_jar); +// +// context.set_block_timestamp_in_days(182); +// +// let mut interest = context.contract().get_total_interest(alice.clone()).amount.total.0; +// assert_eq!(interest, 5_983_561); +// +// context.switch_account(&alice); +// context.contract().claim_total(None); +// +// context.set_block_timestamp_in_days(365); +// +// interest = context.contract().get_total_interest(alice.clone()).amount.total.0; +// assert_eq!(interest, 6_016_439); +// } +// +// #[test] +// #[should_panic(expected = "Penalty is not applicable for constant APY")] +// fn penalty_is_not_applicable_for_constant_apy() { +// let alice = alice(); +// let admin = admin(); +// +// let signer = MessageSigner::new(); +// let product = Product::new() +// .apy(Apy::Constant(UDecimal::new(20, 2))) +// .public_key(signer.public_key()); +// let reference_jar = Jar::new(0).principal(100_000_000); +// +// let mut context = Context::new(admin.clone()) +// .with_products(&[product]) +// .with_jars(&[reference_jar]); +// +// context.switch_account(&admin); +// context.contract().set_penalty(alice, U32(0), true); +// } +// +// #[test] +// fn get_total_interest_for_premium_with_penalty_after_half_term() { +// let alice = alice(); +// let admin = admin(); +// +// let signer = MessageSigner::new(); +// let product = Product::new() +// .apy(Apy::Downgradable(DowngradableApy { +// default: UDecimal::new(20, 2), +// fallback: UDecimal::new(10, 2), +// })) +// .public_key(signer.public_key()); +// let reference_jar = Jar::new(0).principal(100_000_000); +// +// let mut context = Context::new(admin.clone()) +// .with_products(&[product]) +// .with_jars(&[reference_jar]); +// +// context.set_block_timestamp_in_ms(15_768_000_000); +// +// let mut interest = context.contract().get_total_interest(alice.clone()).amount.total.0; +// assert_eq!(interest, 10_000_000); +// +// context.switch_account(&admin); +// context.contract().set_penalty(alice.clone(), U32(0), true); +// +// context.set_block_timestamp_in_ms(31_536_000_000); +// +// interest = context.contract().get_total_interest(alice).amount.total.0; +// assert_eq!(interest, 15_000_000); +// } +// +// #[test] +// fn get_total_interest_for_premium_with_multiple_penalties_applied() { +// let alice = alice(); +// let admin = admin(); +// +// let signer = MessageSigner::new(); +// let product = Product::new() +// .apy(Apy::Downgradable(DowngradableApy { +// default: UDecimal::new(23, 2), +// fallback: UDecimal::new(10, 2), +// })) +// .lockup_term(3_600_000) +// .public_key(signer.public_key()); +// let reference_jar = Jar::new(0).principal(100_000_000_000_000_000_000_000); +// +// let mut context = Context::new(admin.clone()) +// .with_products(&[product]) +// .with_jars(&[reference_jar]); +// +// let products = context.contract().get_products(); +// assert!(matches!(products.first().unwrap().apy, ApyView::Downgradable(_))); +// +// context.switch_account(&admin); +// +// context.set_block_timestamp_in_ms(270_000); +// context.contract().set_penalty(alice.clone(), U32(0), true); +// +// context.set_block_timestamp_in_ms(390_000); +// context.contract().set_penalty(alice.clone(), U32(0), false); +// +// context.set_block_timestamp_in_ms(1_264_000); +// context.contract().set_penalty(alice.clone(), U32(0), true); +// +// context.set_block_timestamp_in_ms(3_700_000); +// +// let interest = context.contract().get_total_interest(alice.clone()).amount.total.0; +// assert_eq!(interest, 1_613_140_537_798_072_044); +// } +// +// #[test] +// fn apply_penalty_in_batch() { +// let admin = admin(); +// let alice = alice(); +// let bob = bob(); +// +// let signer = MessageSigner::new(); +// let product = Product::new() +// .apy(Apy::Downgradable(DowngradableApy { +// default: UDecimal::new(20, 2), +// fallback: UDecimal::new(10, 2), +// })) +// .public_key(signer.public_key()); +// +// let alice_jars = (0..100).map(|id| Jar::new(id).principal(100_000_000)); +// let bob_jars = (0..50).map(|id| Jar::new(id + 200).account_id(&bob).principal(100_000_000)); +// +// let mut context = Context::new(admin.clone()) +// .with_products(&[product]) +// .with_jars(&alice_jars.chain(bob_jars).collect::>()); +// +// context.set_block_timestamp_in_days(182); +// +// let interest = context.contract().get_total_interest(alice.clone()).amount.total.0; +// assert_eq!(interest, 997_260_200); +// +// let interest = context.contract().get_total_interest(bob.clone()).amount.total.0; +// assert_eq!(interest, 498_630_100); +// +// context.switch_account(&admin); +// +// let alice_jars = context +// .contract() +// .get_jars_for_account(alice.clone()) +// .into_iter() +// .map(|j| j.id) +// .collect(); +// let bob_jars = context +// .contract() +// .get_jars_for_account(bob.clone()) +// .into_iter() +// .map(|j| j.id) +// .collect(); +// +// context +// .contract() +// .batch_set_penalty(vec![(alice.clone(), alice_jars), (bob.clone(), bob_jars)], true); +// +// context.set_block_timestamp_in_days(365); +// +// let interest = context.contract().get_total_interest(alice.clone()).amount.total.0; +// assert_eq!(interest, 1_498_630_100); +// +// let interest = context.contract().get_total_interest(bob.clone()).amount.total.0; +// assert_eq!(interest, 749_315_050); +// +// let alice_jars = context.contract().get_jars_for_account(alice); +// let bob_jars = context.contract().get_jars_for_account(bob); +// +// assert!(alice_jars.into_iter().chain(bob_jars).all(|jar| jar.is_penalty_applied)); +// } +// +// #[test] +// fn get_interest_after_withdraw() { +// let alice = alice(); +// let admin = admin(); +// +// let product = Product::new(); +// let jar = Jar::new(0).principal(100_000_000); +// +// let mut context = Context::new(admin).with_products(&[product]).with_jars(&[jar.clone()]); +// +// context.set_block_timestamp_in_days(400); +// +// context.switch_account(&alice); +// context.contract().withdraw(U32(jar.id), None); +// +// let interest = context.contract().get_total_interest(alice.clone()); +// assert_eq!(12_000_000, interest.amount.total.0); +// } +// +// #[test] +// #[should_panic(expected = "Can be performed only by admin")] +// fn unlock_not_by_manager() { +// let alice = alice(); +// let admin = admin(); +// +// let reference_product = Product::new(); +// +// let mut reference_jar = Jar::new(0).product_id(&reference_product.id).principal(100); +// reference_jar.is_pending_withdraw = true; +// let jars = &[reference_jar]; +// +// let mut context = Context::new(admin).with_products(&[reference_product]).with_jars(jars); +// +// context.switch_account(&alice); +// context.contract().unlock_jars_for_account(alice); +// } +// +// #[test] +// fn unlock_by_manager() { +// let alice = alice(); +// let admin = admin(); +// +// let reference_product = Product::new(); +// +// let reference_jar_id = 0; +// let mut reference_jar = Jar::new(0).product_id(&reference_product.id).principal(100); +// reference_jar.is_pending_withdraw = true; +// let jars = &[reference_jar]; +// +// let mut context = Context::new(admin.clone()) +// .with_products(&[reference_product]) +// .with_jars(jars); +// +// assert!( +// context +// .contract() +// .get_jar(alice.clone(), reference_jar_id.into()) +// .is_pending_withdraw +// ); +// +// context.switch_account(&admin); +// context.contract().unlock_jars_for_account(alice.clone()); +// +// assert!( +// !context +// .contract() +// .get_jar(alice.clone(), reference_jar_id.into()) +// .is_pending_withdraw +// ); +// } +// +// #[test] +// fn test_u32() { +// let n = U32(12345678); +// +// assert_eq!(n, from_str(&to_string(&n).unwrap()).unwrap()); +// assert_eq!(U32::from(12345678_u32), n); +// } +// +// #[test] +// fn claim_often_vs_claim_once() { +// fn test(mut product: Product, principal: TokenAmount, days: u64, n: usize) { +// set_test_log_events(false); +// +// let alice: AccountId = format!("alice_{principal}_{days}_{n}").try_into().unwrap(); +// let bob: AccountId = format!("bob_{principal}_{days}_{n}").try_into().unwrap(); +// let admin: AccountId = format!("admin_{principal}_{days}_{n}").try_into().unwrap(); +// +// product.id = format!("product_{principal}_{days}_{n}"); +// +// let alice_jar = Jar::new(0) +// .product_id(&product.id) +// .account_id(&alice) +// .principal(principal); +// +// let bob_jar = Jar::new(1) +// .product_id(&product.id) +// .account_id(&bob) +// .principal(principal); +// +// let mut context = Context::new(admin) +// .with_products(&[product]) +// .with_jars(&[alice_jar.clone(), bob_jar.clone()]); +// +// let mut bobs_claimed = 0; +// +// context.switch_account(&bob); +// +// for day in 0..days { +// context.set_block_timestamp_in_days(day); +// let claimed = context.contract().claim_total(None).unwrap(); +// bobs_claimed += claimed.get_total().0; +// } +// +// let alice_interest = context.contract().get_total_interest(alice.clone()).amount.total.0; +// +// assert_eq!(alice_interest, bobs_claimed); +// } +// +// let product = Product::new(); +// +// test(product.clone(), 10_000_000_000_000_000_000_000_000_000, 365, 0); +// +// for n in 1..10 { +// test( +// product.clone(), +// (1..10_000_000_000_000_000_000_000_000_000).fake(), +// (1..365).fake(), +// n, +// ); +// } +// } diff --git a/contract/src/withdraw/api.rs b/contract/src/withdraw/api.rs index d7da68fa..3cde7785 100644 --- a/contract/src/withdraw/api.rs +++ b/contract/src/withdraw/api.rs @@ -33,12 +33,10 @@ pub struct BulkWithdrawalRequest { #[cfg(not(test))] use crate::ft_interface::FungibleTokenInterface; use crate::{ - assert::assert_not_locked, common, common::gas_data::{GAS_FOR_BULK_AFTER_WITHDRAW, GAS_FOR_FT_TRANSFER}, env, event::{emit, EventKind}, - jar::{account::v2::AccountV2, model::JarV2}, AccountId, Contract, ContractExt, }; @@ -57,14 +55,10 @@ impl WithdrawApi for Contract { let account_id = env::predecessor_account_id(); self.migrate_account_if_needed(&account_id); - let account = self.get_account_mut(&account_id); - - let jar = account.get_jar_mut(&product_id); - assert_not_locked(jar); - jar.lock(); - - self.update_jar_cache(account, &product_id); + self.get_account_mut(&account_id).get_jar_mut(&product_id).try_lock(); + self.update_jar_cache(&account_id, &product_id); + let jar = self.get_account(&account_id).get_jar(&product_id); let product = self.get_product(&product_id); let (amount, partition_index) = jar.get_liquid_balance(&product.terms, env::block_timestamp_ms()); let fee = product.calculate_fee(amount); @@ -82,34 +76,36 @@ impl WithdrawApi for Contract { fn withdraw_all(&mut self) -> PromiseOrValue { let account_id = env::predecessor_account_id(); self.migrate_account_if_needed(&account_id); + + self.update_account_cache(&account_id); + let now = env::block_timestamp_ms(); + let mut request = BulkWithdrawalRequest::default(); - let account = self.get_account_mut(&account_id); - self.update_cache(account); - - let request: BulkWithdrawalRequest = account - .jars - .iter_mut() - .filter(|(_, jar)| !jar.is_pending_withdraw) - .fold( - BulkWithdrawalRequest::default(), - |acc: &mut BulkWithdrawalRequest, (product_id, jar)| { - let product = self.get_product(&product_id); - jar.lock(); - - let (amount, partition_index) = jar.get_liquid_balance(product.terms, now); - let fee = product.calculate_fee(amount); - - acc.requests.push(WithdrawalRequest { - amount, - partition_index, - product_id, - fee, - }); - acc.total_amount += amount; - acc.total_fee += fee; - }, - ); + for (product_id, jar) in &self.get_account(&account_id).jars { + if jar.is_pending_withdraw { + continue; + } + + let product = self.get_product(product_id); + let (amount, partition_index) = jar.get_liquid_balance(&product.terms, now); + let fee = product.calculate_fee(amount); + + request.requests.push(WithdrawalRequest { + amount, + partition_index, + product_id: product.id, + fee, + }); + request.total_amount += amount; + request.total_fee += fee; + } + + for request in request.requests.iter() { + self.get_account_mut(&account_id) + .get_jar_mut(&request.product_id) + .lock(); + } if request.requests.is_empty() { return PromiseOrValue::Value(BulkWithdrawView::default()); @@ -127,14 +123,13 @@ impl Contract { is_promise_success: bool, ) -> WithdrawView { let account = self.get_account_mut(&account_id); - let jar = account.get_jar_mut(&request.product_id); - jar.unlock(); + account.get_jar_mut(&request.product_id).unlock(); if !is_promise_success { return WithdrawView::new(0, None); } - clean_up(&request, account, jar); + self.clean_up(&account_id, &request); let withdrawal_result = WithdrawView::new(request.amount, self.wrap_fee(request.fee)); @@ -153,14 +148,13 @@ impl Contract { request: BulkWithdrawalRequest, is_promise_success: bool, ) -> BulkWithdrawView { - let account = self.get_account_mut(&account_id); - let mut withdrawal_result = BulkWithdrawView { total_amount: 0.into(), withdrawals: vec![], }; if !is_promise_success { + let account = self.get_account_mut(&account_id); for request in request.requests { let jar = account.get_jar_mut(&request.product_id); jar.unlock(); @@ -171,16 +165,15 @@ impl Contract { let mut event_data = vec![]; - for request in request.requests { - let jar = account.get_jar_mut(&request.product_id); - jar.unlock(); + for request in &request.requests { + self.get_account_mut(&account_id) + .get_jar_mut(&request.product_id) + .unlock(); - clean_up(&request, account, jar); - - let deposit_withdrawal = WithdrawView::new(request.amount, self.wrap_fee(request.fee)); + let deposit_withdrawal = WithdrawView::new(request.amount.clone(), self.wrap_fee(request.fee.clone())); event_data.push(( - request.product_id, + request.product_id.clone(), deposit_withdrawal.fee, deposit_withdrawal.withdrawn_amount, )); @@ -189,6 +182,10 @@ impl Contract { withdrawal_result.withdrawals.push(deposit_withdrawal); } + for request in &request.requests { + self.clean_up(&account_id, request); + } + emit(EventKind::WithdrawAll(event_data)); withdrawal_result @@ -206,11 +203,15 @@ impl Contract { } } -fn clean_up(request: &WithdrawalRequest, account: &mut AccountV2, jar: &mut JarV2) { - jar.clean_up_deposits(request.partition_index); +impl Contract { + fn clean_up(&mut self, account_id: &AccountId, request: &WithdrawalRequest) { + let jar = self.get_account_mut(account_id).get_jar_mut(&request.product_id); + jar.clean_up_deposits(request.partition_index); - if jar.should_close() { - account.jars.remove(&request.product_id); + let jar = self.get_account(account_id).get_jar(&request.product_id); + if jar.should_close() { + self.get_account_mut(account_id).jars.remove(&request.product_id); + } } } diff --git a/contract/src/withdraw/tests.rs b/contract/src/withdraw/tests.rs index 6ef8e75c..78e50c7b 100644 --- a/contract/src/withdraw/tests.rs +++ b/contract/src/withdraw/tests.rs @@ -1,407 +1,407 @@ -#![cfg(test)] - -use near_sdk::{json_types::U128, test_utils::test_env::alice, AccountId}; -use sweat_jar_model::{ - api::{ClaimApi, JarApi, WithdrawApi}, - UDecimal, MS_IN_YEAR, U32, -}; - -use crate::{ - common::{test_data::set_test_future_success, tests::Context, Timestamp}, - jar::model::Jar, - product::model::{Apy, Product, WithdrawalFee}, - test_utils::{admin, expect_panic, UnwrapPromise, PRINCIPAL}, - withdraw::api::WithdrawalRequest, -}; - -fn prepare_jar(product: &Product) -> (AccountId, Jar, Context) { - let alice = alice(); - let admin = admin(); - - let jar = Jar::new(0); - let context = Context::new(admin) - .with_products(&[product.clone()]) - .with_jars(&[jar.clone()]); - - (alice, jar, context) -} - -fn prepare_jar_created_at(product: &Product, created_at: Timestamp) -> (AccountId, Jar, Context) { - let alice = alice(); - let admin = admin(); - - let jar = Jar::new(0).created_at(created_at); - let context = Context::new(admin) - .with_products(&[product.clone()]) - .with_jars(&[jar.clone()]); - - (alice, jar, context) -} - -#[test] -fn withdraw_locked_jar_before_maturity_by_not_owner() { - let (_, _, context) = prepare_jar(&Product::new()); - - expect_panic(&context, "Account 'owner' doesn't exist", || { - context.contract().withdraw(U32(0), None); - }); - - assert_eq!(context.contract().withdraw_all(None).unwrap().total_amount.0, 0); -} - -#[test] -fn withdraw_locked_jar_before_maturity_by_owner() { - let (alice, jar, mut context) = prepare_jar_created_at(&Product::new().lockup_term(200), 100); - - context.set_block_timestamp_in_ms(120); - - context.switch_account(&alice); - - expect_panic(&context, "The jar is not mature yet", || { - context.contract().withdraw(U32(jar.id), None); - }); - - assert!(context.contract().withdraw_all(None).unwrap().jars.is_empty()); -} - -#[test] -fn withdraw_locked_jar_after_maturity_by_not_owner() { - let product = Product::new(); - let (_, jar, mut context) = prepare_jar(&product); - - context.set_block_timestamp_in_ms(product.get_lockup_term().unwrap() + 1); - - expect_panic(&context, "Account 'owner' doesn't exist", || { - context.contract().withdraw(U32(jar.id), None); - }); - - assert_eq!(context.contract().withdraw_all(None).unwrap().total_amount.0, 0); -} - -#[test] -fn withdraw_locked_jar_after_maturity_by_owner() { - let product = Product::new(); - let (alice, jar, mut context) = prepare_jar(&product); - - context.set_block_timestamp_in_ms(product.get_lockup_term().unwrap() + 1); - context.switch_account(&alice); - context.contract().withdraw(U32(jar.id), None); -} - -#[test] -#[should_panic(expected = "Account 'owner' doesn't exist")] -fn withdraw_flexible_jar_by_not_owner() { - let product = Product::new().flexible(); - let (_, jar, mut context) = prepare_jar(&product); - - context.set_block_timestamp_in_days(1); - context.contract().withdraw(U32(jar.id), None); -} - -#[test] -fn withdraw_flexible_jar_by_owner_full() { - let product = Product::new().flexible(); - let (alice, reference_jar, mut context) = prepare_jar(&product); - - context.set_block_timestamp_in_days(1); - context.switch_account(&alice); - - context.contract().withdraw(U32(reference_jar.id), None); - - let interest = context - .contract() - .get_interest(vec![reference_jar.id.into()], alice.clone()); - - let claimed = context.contract().claim_total(None).unwrap(); - - assert_eq!(interest.amount.total, claimed.get_total()); - - let jar = context.contract().get_jar(alice.clone(), U32(reference_jar.id)); - assert_eq!(0, jar.principal.0); -} - -#[test] -fn withdraw_flexible_jar_by_owner_with_sufficient_balance() { - let product = Product::new().flexible(); - let (alice, reference_jar, mut context) = prepare_jar(&product); - - context.set_block_timestamp_in_days(1); - context.switch_account(&alice); - - context.contract().withdraw(U32(0), Some(U128(100_000))); - let jar = context.contract().get_jar(alice.clone(), U32(reference_jar.id)); - assert_eq!(900_000, jar.principal.0); -} - -#[test] -fn withdraw_flexible_jar_by_owner_with_insufficient_balance() { - let product = Product::new().flexible(); - let (alice, jar, mut context) = prepare_jar(&product); - - context.set_block_timestamp_in_days(1); - context.switch_account(&alice); - - expect_panic(&context, "Insufficient balance", || { - context.contract().withdraw(U32(jar.id), Some(U128(2_000_000))); - }); - - let withdrawn = context.contract().withdraw_all(None).unwrap(); - - assert_eq!(withdrawn.jars.len(), 1); - assert_eq!(withdrawn.jars[0].withdrawn_amount.0, 1_000_000); -} - -#[test] -fn dont_delete_jar_after_withdraw_with_interest_left() { - let product = Product::new().apy(Apy::Constant(UDecimal::new(2, 1))); - - let (alice, _, mut context) = prepare_jar(&product); - - context.set_block_timestamp_in_ms(product.get_lockup_term().unwrap() + 1); - context.switch_account(&alice); - - let jar = context.contract().get_jar_internal(&alice, 0); - - let withdrawn = context.contract().withdraw(U32(jar.id), Some(U128(1_000_000))).unwrap(); - - assert_eq!(withdrawn.withdrawn_amount, U128(1_000_000)); - assert_eq!(withdrawn.fee, U128(0)); - - let jar = context.contract().get_jar_internal(&alice, 0); - assert_eq!(jar.principal, 0); - - assert_eq!(jar.cache.as_ref().unwrap().interest, 200_000); -} - -#[test] -fn product_with_fixed_fee() { - let fee = 10; - let product = Product::new().with_withdrawal_fee(WithdrawalFee::Fix(fee)); - let (alice, reference_jar, mut context) = prepare_jar(&product); - - let initial_principal = reference_jar.principal; - - context.set_block_timestamp_in_ms(product.get_lockup_term().unwrap() + 1); - context.switch_account(&alice); - - let withdraw_amount = 100_000; - let withdraw = context - .contract() - .withdraw(U32(0), Some(U128(withdraw_amount))) - .unwrap(); - - assert_eq!(withdraw.withdrawn_amount, U128(withdraw_amount - fee)); - assert_eq!(withdraw.fee, U128(fee)); - - let jar = context.contract().get_jar(alice, U32(reference_jar.id)); - - assert_eq!(jar.principal, U128(initial_principal - withdraw_amount)); -} - -#[test] -fn product_with_percent_fee() { - let fee_value = UDecimal::new(5, 4); - let fee = WithdrawalFee::Percent(fee_value.clone()); - let product = Product::new().with_withdrawal_fee(fee); - let (alice, reference_jar, mut context) = prepare_jar(&product); - - let initial_principal = reference_jar.principal; - - context.set_block_timestamp_in_ms(product.get_lockup_term().unwrap() + 1); - context.switch_account(&alice); - - let withdrawn_amount = 100_000; - let withdraw = context - .contract() - .withdraw(U32(0), Some(U128(withdrawn_amount))) - .unwrap(); - - let reference_fee = fee_value * initial_principal; - assert_eq!(withdraw.withdrawn_amount, U128(withdrawn_amount - reference_fee)); - assert_eq!(withdraw.fee, U128(reference_fee)); - - let jar = context.contract().get_jar(alice, U32(reference_jar.id)); - - assert_eq!(jar.principal, U128(initial_principal - withdrawn_amount)); -} - -#[test] -fn test_failed_withdraw_promise() { - set_test_future_success(false); - - let product = Product::new(); - let (alice, reference_jar, mut context) = prepare_jar(&product); - - context.set_block_timestamp_in_ms(product.get_lockup_term().unwrap() + 1); - context.switch_account(&alice); - - let jar_before_withdrawal = context.contract().get_jar(alice.clone(), U32(reference_jar.id)); - - let withdrawn = context.contract().withdraw(U32(0), Some(U128(100_000))).unwrap(); - - assert_eq!(withdrawn.withdrawn_amount.0, 0); - - let jar_after_withdrawal = context.contract().get_jar(alice.clone(), U32(reference_jar.id)); - - assert_eq!(jar_before_withdrawal, jar_after_withdrawal); -} - -#[test] -fn test_failed_withdraw_internal() { - let product = Product::new(); - let (alice, reference_jar, context) = prepare_jar(&product); - let withdrawn_amount = 1_234; - - let mut contract = context.contract(); - - let jar_view = contract.get_jar(alice.clone(), U32(reference_jar.id)); - let jar = contract.accounts.get(&alice).unwrap().iter().next().unwrap().clone(); - - let withdraw = - contract.after_withdraw_internal(jar.account_id.clone(), jar.id, true, withdrawn_amount, None, false); - - assert_eq!(withdraw.withdrawn_amount, U128(0)); - assert_eq!(withdraw.fee, U128(0)); - - assert_eq!( - jar_view.principal.0 + withdrawn_amount, - contract.get_jar(alice, U32(0)).principal.0 - ); -} - -#[test] -fn test_failed_bulk_withdraw_internal() { - let product = Product::new(); - let (alice, reference_jar, context) = prepare_jar(&product); - - let mut contract = context.contract(); - - let jar_view = contract.get_jar(alice.clone(), U32(reference_jar.id)); - let jar = contract.accounts.get(&alice).unwrap().iter().next().unwrap().clone(); - - let withdraw = contract.after_bulk_withdraw_internal( - jar.account_id.clone(), - vec![WithdrawalRequest { - jar: jar.clone(), - should_be_closed: true, - amount: jar.principal, - fee: None, - }], - false, - ); - - assert!(withdraw.jars.is_empty()); - assert_eq!(withdraw.total_amount.0, 0); - - assert_eq!( - jar_view.principal.0 + jar_view.principal.0, - contract.get_jar(alice, U32(0)).principal.0 - ); -} - -#[test] -fn withdraw_from_locked_jar() { - let product = Product::new().apy(Apy::Constant(UDecimal::new(1, 0))); - let mut jar = Jar::new(0).principal(MS_IN_YEAR as u128); - - jar.lock(); - - let alice = alice(); - let admin = admin(); - - let mut context = Context::new(admin).with_products(&[product.clone()]).with_jars(&[jar]); - - context.set_block_timestamp_in_ms(product.get_lockup_term().unwrap() + 1); - context.switch_account(&alice); - - expect_panic(&context, "Another operation on this Jar is in progress", || { - _ = context.contract().withdraw(U32(0), Some(U128(100_000))); - }); - - assert!(context.contract().withdraw_all(None).unwrap().jars.is_empty()); -} - -#[test] -fn withdraw_all() { - let alice = alice(); - let admin = admin(); - - let product = Product::new(); - let long_term_product = Product::new().id("long_term_product").lockup_term(MS_IN_YEAR * 2); - - let mature_jar = Jar::new(1).principal(PRINCIPAL + 1); - - let immature_jar = Jar::new(2).product_id(&long_term_product.id).principal(PRINCIPAL + 3); - let locked_jar = Jar::new(3).product_id(&product.id).principal(PRINCIPAL + 4).locked(); - - let mut context = Context::new(admin) - .with_products(&[product, long_term_product]) - .with_jars(&[mature_jar.clone(), immature_jar.clone(), locked_jar.clone()]); - - context.set_block_timestamp_in_days(366); - - context.switch_account(&alice); - - context.contract().claim_total(None); - - let withdrawn_jars = context.contract().withdraw_all(None).unwrap(); - - assert_eq!(withdrawn_jars.total_amount.0, 1000001); - - assert_eq!( - withdrawn_jars - .jars - .iter() - .map(|j| j.withdrawn_amount.0) - .collect::>(), - vec![mature_jar.principal] - ); - - let all_jars = context.contract().get_jars_for_account(alice); - - assert_eq!( - all_jars.iter().map(|j| j.principal.0).collect::>(), - vec![locked_jar.principal, immature_jar.principal] - ); - - assert_eq!( - all_jars.iter().map(|j| j.id.0).collect::>(), - vec![locked_jar.id, immature_jar.id,] - ); -} - -#[test] -fn batch_withdraw_all() { - let alice = alice(); - let admin = admin(); - - let product = Product::new().enabled(true).lockup_term(MS_IN_YEAR); - - let jars: Vec<_> = (0..8) - .map(|id| Jar::new(id).principal(PRINCIPAL + id as u128)) - .collect(); - - let mut context = Context::new(admin).with_products(&[product]).with_jars(&jars); - - context.set_block_timestamp_in_days(366); - - context.switch_account(&alice); - - context.contract().claim_total(None); - - let withdrawn_jars = context - .contract() - .withdraw_all(Some(vec![1.into(), 3.into(), 5.into()])) - .unwrap(); - - assert_eq!(withdrawn_jars.total_amount.0, 3000009); - - let jars: Vec<_> = context - .contract() - .get_jars_for_account(alice) - .into_iter() - .map(|j| j.id.0) - .collect(); - - assert_eq!(jars, [0, 7, 2, 6, 4,]); -} +// #![cfg(test)] +// +// use near_sdk::{json_types::U128, test_utils::test_env::alice, AccountId}; +// use sweat_jar_model::{ +// api::{ClaimApi, JarApi, WithdrawApi}, +// UDecimal, MS_IN_YEAR, U32, +// }; +// +// use crate::{ +// common::{test_data::set_test_future_success, tests::Context, Timestamp}, +// jar::model::Jar, +// product::model::{Apy, Product, WithdrawalFee}, +// test_utils::{admin, expect_panic, UnwrapPromise, PRINCIPAL}, +// withdraw::api::WithdrawalRequest, +// }; +// +// fn prepare_jar(product: &Product) -> (AccountId, Jar, Context) { +// let alice = alice(); +// let admin = admin(); +// +// let jar = Jar::new(0); +// let context = Context::new(admin) +// .with_products(&[product.clone()]) +// .with_jars(&[jar.clone()]); +// +// (alice, jar, context) +// } +// +// fn prepare_jar_created_at(product: &Product, created_at: Timestamp) -> (AccountId, Jar, Context) { +// let alice = alice(); +// let admin = admin(); +// +// let jar = Jar::new(0).created_at(created_at); +// let context = Context::new(admin) +// .with_products(&[product.clone()]) +// .with_jars(&[jar.clone()]); +// +// (alice, jar, context) +// } +// +// #[test] +// fn withdraw_locked_jar_before_maturity_by_not_owner() { +// let (_, _, context) = prepare_jar(&Product::new()); +// +// expect_panic(&context, "Account 'owner' doesn't exist", || { +// context.contract().withdraw(U32(0), None); +// }); +// +// assert_eq!(context.contract().withdraw_all(None).unwrap().total_amount.0, 0); +// } +// +// #[test] +// fn withdraw_locked_jar_before_maturity_by_owner() { +// let (alice, jar, mut context) = prepare_jar_created_at(&Product::new().lockup_term(200), 100); +// +// context.set_block_timestamp_in_ms(120); +// +// context.switch_account(&alice); +// +// expect_panic(&context, "The jar is not mature yet", || { +// context.contract().withdraw(U32(jar.id), None); +// }); +// +// assert!(context.contract().withdraw_all(None).unwrap().jars.is_empty()); +// } +// +// #[test] +// fn withdraw_locked_jar_after_maturity_by_not_owner() { +// let product = Product::new(); +// let (_, jar, mut context) = prepare_jar(&product); +// +// context.set_block_timestamp_in_ms(product.get_lockup_term().unwrap() + 1); +// +// expect_panic(&context, "Account 'owner' doesn't exist", || { +// context.contract().withdraw(U32(jar.id), None); +// }); +// +// assert_eq!(context.contract().withdraw_all(None).unwrap().total_amount.0, 0); +// } +// +// #[test] +// fn withdraw_locked_jar_after_maturity_by_owner() { +// let product = Product::new(); +// let (alice, jar, mut context) = prepare_jar(&product); +// +// context.set_block_timestamp_in_ms(product.get_lockup_term().unwrap() + 1); +// context.switch_account(&alice); +// context.contract().withdraw(U32(jar.id), None); +// } +// +// #[test] +// #[should_panic(expected = "Account 'owner' doesn't exist")] +// fn withdraw_flexible_jar_by_not_owner() { +// let product = Product::new().flexible(); +// let (_, jar, mut context) = prepare_jar(&product); +// +// context.set_block_timestamp_in_days(1); +// context.contract().withdraw(U32(jar.id), None); +// } +// +// #[test] +// fn withdraw_flexible_jar_by_owner_full() { +// let product = Product::new().flexible(); +// let (alice, reference_jar, mut context) = prepare_jar(&product); +// +// context.set_block_timestamp_in_days(1); +// context.switch_account(&alice); +// +// context.contract().withdraw(U32(reference_jar.id), None); +// +// let interest = context +// .contract() +// .get_interest(vec![reference_jar.id.into()], alice.clone()); +// +// let claimed = context.contract().claim_total(None).unwrap(); +// +// assert_eq!(interest.amount.total, claimed.get_total()); +// +// let jar = context.contract().get_jar(alice.clone(), U32(reference_jar.id)); +// assert_eq!(0, jar.principal.0); +// } +// +// #[test] +// fn withdraw_flexible_jar_by_owner_with_sufficient_balance() { +// let product = Product::new().flexible(); +// let (alice, reference_jar, mut context) = prepare_jar(&product); +// +// context.set_block_timestamp_in_days(1); +// context.switch_account(&alice); +// +// context.contract().withdraw(U32(0), Some(U128(100_000))); +// let jar = context.contract().get_jar(alice.clone(), U32(reference_jar.id)); +// assert_eq!(900_000, jar.principal.0); +// } +// +// #[test] +// fn withdraw_flexible_jar_by_owner_with_insufficient_balance() { +// let product = Product::new().flexible(); +// let (alice, jar, mut context) = prepare_jar(&product); +// +// context.set_block_timestamp_in_days(1); +// context.switch_account(&alice); +// +// expect_panic(&context, "Insufficient balance", || { +// context.contract().withdraw(U32(jar.id), Some(U128(2_000_000))); +// }); +// +// let withdrawn = context.contract().withdraw_all(None).unwrap(); +// +// assert_eq!(withdrawn.jars.len(), 1); +// assert_eq!(withdrawn.jars[0].withdrawn_amount.0, 1_000_000); +// } +// +// #[test] +// fn dont_delete_jar_after_withdraw_with_interest_left() { +// let product = Product::new().apy(Apy::Constant(UDecimal::new(2, 1))); +// +// let (alice, _, mut context) = prepare_jar(&product); +// +// context.set_block_timestamp_in_ms(product.get_lockup_term().unwrap() + 1); +// context.switch_account(&alice); +// +// let jar = context.contract().get_jar_internal(&alice, 0); +// +// let withdrawn = context.contract().withdraw(U32(jar.id), Some(U128(1_000_000))).unwrap(); +// +// assert_eq!(withdrawn.withdrawn_amount, U128(1_000_000)); +// assert_eq!(withdrawn.fee, U128(0)); +// +// let jar = context.contract().get_jar_internal(&alice, 0); +// assert_eq!(jar.principal, 0); +// +// assert_eq!(jar.cache.as_ref().unwrap().interest, 200_000); +// } +// +// #[test] +// fn product_with_fixed_fee() { +// let fee = 10; +// let product = Product::new().with_withdrawal_fee(WithdrawalFee::Fix(fee)); +// let (alice, reference_jar, mut context) = prepare_jar(&product); +// +// let initial_principal = reference_jar.principal; +// +// context.set_block_timestamp_in_ms(product.get_lockup_term().unwrap() + 1); +// context.switch_account(&alice); +// +// let withdraw_amount = 100_000; +// let withdraw = context +// .contract() +// .withdraw(U32(0), Some(U128(withdraw_amount))) +// .unwrap(); +// +// assert_eq!(withdraw.withdrawn_amount, U128(withdraw_amount - fee)); +// assert_eq!(withdraw.fee, U128(fee)); +// +// let jar = context.contract().get_jar(alice, U32(reference_jar.id)); +// +// assert_eq!(jar.principal, U128(initial_principal - withdraw_amount)); +// } +// +// #[test] +// fn product_with_percent_fee() { +// let fee_value = UDecimal::new(5, 4); +// let fee = WithdrawalFee::Percent(fee_value.clone()); +// let product = Product::new().with_withdrawal_fee(fee); +// let (alice, reference_jar, mut context) = prepare_jar(&product); +// +// let initial_principal = reference_jar.principal; +// +// context.set_block_timestamp_in_ms(product.get_lockup_term().unwrap() + 1); +// context.switch_account(&alice); +// +// let withdrawn_amount = 100_000; +// let withdraw = context +// .contract() +// .withdraw(U32(0), Some(U128(withdrawn_amount))) +// .unwrap(); +// +// let reference_fee = fee_value * initial_principal; +// assert_eq!(withdraw.withdrawn_amount, U128(withdrawn_amount - reference_fee)); +// assert_eq!(withdraw.fee, U128(reference_fee)); +// +// let jar = context.contract().get_jar(alice, U32(reference_jar.id)); +// +// assert_eq!(jar.principal, U128(initial_principal - withdrawn_amount)); +// } +// +// #[test] +// fn test_failed_withdraw_promise() { +// set_test_future_success(false); +// +// let product = Product::new(); +// let (alice, reference_jar, mut context) = prepare_jar(&product); +// +// context.set_block_timestamp_in_ms(product.get_lockup_term().unwrap() + 1); +// context.switch_account(&alice); +// +// let jar_before_withdrawal = context.contract().get_jar(alice.clone(), U32(reference_jar.id)); +// +// let withdrawn = context.contract().withdraw(U32(0), Some(U128(100_000))).unwrap(); +// +// assert_eq!(withdrawn.withdrawn_amount.0, 0); +// +// let jar_after_withdrawal = context.contract().get_jar(alice.clone(), U32(reference_jar.id)); +// +// assert_eq!(jar_before_withdrawal, jar_after_withdrawal); +// } +// +// #[test] +// fn test_failed_withdraw_internal() { +// let product = Product::new(); +// let (alice, reference_jar, context) = prepare_jar(&product); +// let withdrawn_amount = 1_234; +// +// let mut contract = context.contract(); +// +// let jar_view = contract.get_jar(alice.clone(), U32(reference_jar.id)); +// let jar = contract.accounts.get(&alice).unwrap().iter().next().unwrap().clone(); +// +// let withdraw = +// contract.after_withdraw_internal(jar.account_id.clone(), jar.id, true, withdrawn_amount, None, false); +// +// assert_eq!(withdraw.withdrawn_amount, U128(0)); +// assert_eq!(withdraw.fee, U128(0)); +// +// assert_eq!( +// jar_view.principal.0 + withdrawn_amount, +// contract.get_jar(alice, U32(0)).principal.0 +// ); +// } +// +// #[test] +// fn test_failed_bulk_withdraw_internal() { +// let product = Product::new(); +// let (alice, reference_jar, context) = prepare_jar(&product); +// +// let mut contract = context.contract(); +// +// let jar_view = contract.get_jar(alice.clone(), U32(reference_jar.id)); +// let jar = contract.accounts.get(&alice).unwrap().iter().next().unwrap().clone(); +// +// let withdraw = contract.after_bulk_withdraw_internal( +// jar.account_id.clone(), +// vec![WithdrawalRequest { +// jar: jar.clone(), +// should_be_closed: true, +// amount: jar.principal, +// fee: None, +// }], +// false, +// ); +// +// assert!(withdraw.jars.is_empty()); +// assert_eq!(withdraw.total_amount.0, 0); +// +// assert_eq!( +// jar_view.principal.0 + jar_view.principal.0, +// contract.get_jar(alice, U32(0)).principal.0 +// ); +// } +// +// #[test] +// fn withdraw_from_locked_jar() { +// let product = Product::new().apy(Apy::Constant(UDecimal::new(1, 0))); +// let mut jar = Jar::new(0).principal(MS_IN_YEAR as u128); +// +// jar.lock(); +// +// let alice = alice(); +// let admin = admin(); +// +// let mut context = Context::new(admin).with_products(&[product.clone()]).with_jars(&[jar]); +// +// context.set_block_timestamp_in_ms(product.get_lockup_term().unwrap() + 1); +// context.switch_account(&alice); +// +// expect_panic(&context, "Another operation on this Jar is in progress", || { +// _ = context.contract().withdraw(U32(0), Some(U128(100_000))); +// }); +// +// assert!(context.contract().withdraw_all(None).unwrap().jars.is_empty()); +// } +// +// #[test] +// fn withdraw_all() { +// let alice = alice(); +// let admin = admin(); +// +// let product = Product::new(); +// let long_term_product = Product::new().id("long_term_product").lockup_term(MS_IN_YEAR * 2); +// +// let mature_jar = Jar::new(1).principal(PRINCIPAL + 1); +// +// let immature_jar = Jar::new(2).product_id(&long_term_product.id).principal(PRINCIPAL + 3); +// let locked_jar = Jar::new(3).product_id(&product.id).principal(PRINCIPAL + 4).locked(); +// +// let mut context = Context::new(admin) +// .with_products(&[product, long_term_product]) +// .with_jars(&[mature_jar.clone(), immature_jar.clone(), locked_jar.clone()]); +// +// context.set_block_timestamp_in_days(366); +// +// context.switch_account(&alice); +// +// context.contract().claim_total(None); +// +// let withdrawn_jars = context.contract().withdraw_all(None).unwrap(); +// +// assert_eq!(withdrawn_jars.total_amount.0, 1000001); +// +// assert_eq!( +// withdrawn_jars +// .jars +// .iter() +// .map(|j| j.withdrawn_amount.0) +// .collect::>(), +// vec![mature_jar.principal] +// ); +// +// let all_jars = context.contract().get_jars_for_account(alice); +// +// assert_eq!( +// all_jars.iter().map(|j| j.principal.0).collect::>(), +// vec![locked_jar.principal, immature_jar.principal] +// ); +// +// assert_eq!( +// all_jars.iter().map(|j| j.id.0).collect::>(), +// vec![locked_jar.id, immature_jar.id,] +// ); +// } +// +// #[test] +// fn batch_withdraw_all() { +// let alice = alice(); +// let admin = admin(); +// +// let product = Product::new().enabled(true).lockup_term(MS_IN_YEAR); +// +// let jars: Vec<_> = (0..8) +// .map(|id| Jar::new(id).principal(PRINCIPAL + id as u128)) +// .collect(); +// +// let mut context = Context::new(admin).with_products(&[product]).with_jars(&jars); +// +// context.set_block_timestamp_in_days(366); +// +// context.switch_account(&alice); +// +// context.contract().claim_total(None); +// +// let withdrawn_jars = context +// .contract() +// .withdraw_all(Some(vec![1.into(), 3.into(), 5.into()])) +// .unwrap(); +// +// assert_eq!(withdrawn_jars.total_amount.0, 3000009); +// +// let jars: Vec<_> = context +// .contract() +// .get_jars_for_account(alice) +// .into_iter() +// .map(|j| j.id.0) +// .collect(); +// +// assert_eq!(jars, [0, 7, 2, 6, 4,]); +// } diff --git a/integration-tests/src/claim_detailed.rs b/integration-tests/src/claim_detailed.rs index 526cec59..37571e3e 100644 --- a/integration-tests/src/claim_detailed.rs +++ b/integration-tests/src/claim_detailed.rs @@ -1,64 +1,64 @@ -use nitka::misc::ToNear; -use sweat_jar_model::{ - api::{ClaimApiIntegration, JarApiIntegration, ProductApiIntegration}, - claimed_amount_view::ClaimedAmountView, -}; - -use crate::{ - context::{prepare_contract, IntegrationContext}, - jar_contract_extensions::JarContractExtensions, - product::RegisterProductCommand, -}; - -#[tokio::test] -async fn claim_detailed() -> anyhow::Result<()> { - println!("👷🏽 Run detailed claim test"); - - let mut context = prepare_contract( - None, - [ - RegisterProductCommand::Locked12Months12Percents, - RegisterProductCommand::Locked6Months6Percents, - RegisterProductCommand::Locked6Months6PercentsWithWithdrawFee, - ], - ) - .await?; - - let alice = context.alice().await?; - - let products = context.sweat_jar().get_products().await?; - assert_eq!(3, products.len()); - - context - .sweat_jar() - .create_jar( - &alice, - RegisterProductCommand::Locked12Months12Percents.id(), - 1_000_000, - &context.ft_contract(), - ) - .await?; - - let alice_principal = context.sweat_jar().get_total_principal(alice.to_near()).await?; - let alice_interest = context.sweat_jar().get_total_interest(alice.to_near()).await?; - assert_eq!(1_000_000, alice_principal.total.0); - assert_eq!(0, alice_interest.amount.total.0); - - context.fast_forward_hours(1).await?; - - let claimed_details = context.sweat_jar().claim_total(Some(true)).with_user(&alice).await?; - - let ClaimedAmountView::Detailed(claimed_details) = claimed_details else { - panic!() - }; - - let claimed_amount = claimed_details.total.0; - - assert!(15 < claimed_amount && claimed_amount < 20); - assert_eq!( - claimed_amount, - claimed_details.detailed.values().map(|item| item.0).sum() - ); - - Ok(()) -} +// use nitka::misc::ToNear; +// use sweat_jar_model::{ +// api::{ClaimApiIntegration, JarApiIntegration, ProductApiIntegration}, +// claimed_amount_view::ClaimedAmountView, +// }; +// +// use crate::{ +// context::{prepare_contract, IntegrationContext}, +// jar_contract_extensions::JarContractExtensions, +// product::RegisterProductCommand, +// }; +// +// #[tokio::test] +// async fn claim_detailed() -> anyhow::Result<()> { +// println!("👷🏽 Run detailed claim test"); +// +// let mut context = prepare_contract( +// None, +// [ +// RegisterProductCommand::Locked12Months12Percents, +// RegisterProductCommand::Locked6Months6Percents, +// RegisterProductCommand::Locked6Months6PercentsWithWithdrawFee, +// ], +// ) +// .await?; +// +// let alice = context.alice().await?; +// +// let products = context.sweat_jar().get_products().await?; +// assert_eq!(3, products.len()); +// +// context +// .sweat_jar() +// .create_jar( +// &alice, +// RegisterProductCommand::Locked12Months12Percents.id(), +// 1_000_000, +// &context.ft_contract(), +// ) +// .await?; +// +// let alice_principal = context.sweat_jar().get_total_principal(alice.to_near()).await?; +// let alice_interest = context.sweat_jar().get_total_interest(alice.to_near()).await?; +// assert_eq!(1_000_000, alice_principal.total.0); +// assert_eq!(0, alice_interest.amount.total.0); +// +// context.fast_forward_hours(1).await?; +// +// let claimed_details = context.sweat_jar().claim_total(Some(true)).with_user(&alice).await?; +// +// let ClaimedAmountView::Detailed(claimed_details) = claimed_details else { +// panic!() +// }; +// +// let claimed_amount = claimed_details.total.0; +// +// assert!(15 < claimed_amount && claimed_amount < 20); +// assert_eq!( +// claimed_amount, +// claimed_details.detailed.values().map(|item| item.0).sum() +// ); +// +// Ok(()) +// } diff --git a/integration-tests/src/happy_flow.rs b/integration-tests/src/happy_flow.rs index 625c9a24..17336f34 100644 --- a/integration-tests/src/happy_flow.rs +++ b/integration-tests/src/happy_flow.rs @@ -1,64 +1,64 @@ -use nitka::misc::ToNear; -use sweat_jar_model::api::{ClaimApiIntegration, JarApiIntegration, ProductApiIntegration}; -use sweat_model::FungibleTokenCoreIntegration; - -use crate::{ - context::{prepare_contract, IntegrationContext}, - jar_contract_extensions::JarContractExtensions, - product::RegisterProductCommand, -}; - -#[tokio::test] -#[mutants::skip] -async fn happy_flow() -> anyhow::Result<()> { - println!("👷🏽 Run happy flow test"); - - let mut context = prepare_contract( - None, - [ - RegisterProductCommand::Locked12Months12Percents, - RegisterProductCommand::Locked6Months6Percents, - RegisterProductCommand::Locked6Months6PercentsWithWithdrawFee, - ], - ) - .await?; - - let alice = context.alice().await?; - - let products = context.sweat_jar().get_products().await?; - assert_eq!(3, products.len()); - - context - .sweat_jar() - .create_jar( - &alice, - RegisterProductCommand::Locked12Months12Percents.id(), - 1_000_000, - &context.ft_contract(), - ) - .await?; - - let alice_principal = context.sweat_jar().get_total_principal(alice.to_near()).await?; - let mut alice_interest = context.sweat_jar().get_total_interest(alice.to_near()).await?; - assert_eq!(1_000_000, alice_principal.total.0); - assert_eq!(0, alice_interest.amount.total.0); - - context.fast_forward_hours(1).await?; - - alice_interest = context.sweat_jar().get_total_interest(alice.to_near()).await?; - assert!(alice_interest.amount.total.0 > 0); - - let claimed_amount = context - .sweat_jar() - .claim_total(None) - .with_user(&alice) - .await? - .get_total() - .0; - assert!(15 < claimed_amount && claimed_amount < 20); - - let alice_balance = context.ft_contract().ft_balance_of(alice.to_near()).await?.0; - assert_eq!(99_000_000 + claimed_amount, alice_balance); - - Ok(()) -} +// use nitka::misc::ToNear; +// use sweat_jar_model::api::{ClaimApiIntegration, JarApiIntegration, ProductApiIntegration}; +// use sweat_model::FungibleTokenCoreIntegration; +// +// use crate::{ +// context::{prepare_contract, IntegrationContext}, +// jar_contract_extensions::JarContractExtensions, +// product::RegisterProductCommand, +// }; +// +// #[tokio::test] +// #[mutants::skip] +// async fn happy_flow() -> anyhow::Result<()> { +// println!("👷🏽 Run happy flow test"); +// +// let mut context = prepare_contract( +// None, +// [ +// RegisterProductCommand::Locked12Months12Percents, +// RegisterProductCommand::Locked6Months6Percents, +// RegisterProductCommand::Locked6Months6PercentsWithWithdrawFee, +// ], +// ) +// .await?; +// +// let alice = context.alice().await?; +// +// let products = context.sweat_jar().get_products().await?; +// assert_eq!(3, products.len()); +// +// context +// .sweat_jar() +// .create_jar( +// &alice, +// RegisterProductCommand::Locked12Months12Percents.id(), +// 1_000_000, +// &context.ft_contract(), +// ) +// .await?; +// +// let alice_principal = context.sweat_jar().get_total_principal(alice.to_near()).await?; +// let mut alice_interest = context.sweat_jar().get_total_interest(alice.to_near()).await?; +// assert_eq!(1_000_000, alice_principal.total.0); +// assert_eq!(0, alice_interest.amount.total.0); +// +// context.fast_forward_hours(1).await?; +// +// alice_interest = context.sweat_jar().get_total_interest(alice.to_near()).await?; +// assert!(alice_interest.amount.total.0 > 0); +// +// let claimed_amount = context +// .sweat_jar() +// .claim_total(None) +// .with_user(&alice) +// .await? +// .get_total() +// .0; +// assert!(15 < claimed_amount && claimed_amount < 20); +// +// let alice_balance = context.ft_contract().ft_balance_of(alice.to_near()).await?.0; +// assert_eq!(99_000_000 + claimed_amount, alice_balance); +// +// Ok(()) +// } diff --git a/integration-tests/src/jar_deletion.rs b/integration-tests/src/jar_deletion.rs index 91b99e11..3f716380 100644 --- a/integration-tests/src/jar_deletion.rs +++ b/integration-tests/src/jar_deletion.rs @@ -1,63 +1,63 @@ -use nitka::misc::ToNear; -use sweat_jar_model::api::{ClaimApiIntegration, JarApiIntegration, WithdrawApiIntegration}; - -use crate::{ - context::{prepare_contract, IntegrationContext}, - jar_contract_extensions::JarContractExtensions, - product::RegisterProductCommand, -}; - -#[tokio::test] -#[mutants::skip] -async fn jar_deletion() -> anyhow::Result<()> { - println!("👷🏽 Run jar deletion test"); - - let mut context = prepare_contract(None, [RegisterProductCommand::Locked10Minutes60000Percents]).await?; - - let alice = context.alice().await?; - - context - .sweat_jar() - .create_jar( - &alice, - RegisterProductCommand::Locked10Minutes60000Percents.id(), - 1_000_000, - &context.ft_contract(), - ) - .await?; - - let jar_view = context - .sweat_jar() - .get_jars_for_account(alice.to_near()) - .await? - .into_iter() - .next() - .unwrap(); - - context.fast_forward_minutes(11).await?; - - let withdrawn_amount = context - .sweat_jar() - .withdraw(jar_view.id, None) - .with_user(&alice) - .await?; - assert_eq!(withdrawn_amount.withdrawn_amount.0, 1_000_000); - - let alice_interest = context.sweat_jar().get_total_interest(alice.to_near()).await?; - let claimed_amount = context - .sweat_jar() - .claim_total(None) - .with_user(&alice) - .await? - .get_total() - .0; - assert_eq!(alice_interest.amount.total.0, claimed_amount); - - let alice_interest = context.sweat_jar().get_total_interest(alice.to_near()).await?; - assert_eq!(alice_interest.amount.total.0, 0); - - let jars = context.sweat_jar().get_jars_for_account(alice.to_near()).await?; - assert!(jars.is_empty()); - - Ok(()) -} +// use nitka::misc::ToNear; +// use sweat_jar_model::api::{ClaimApiIntegration, JarApiIntegration, WithdrawApiIntegration}; +// +// use crate::{ +// context::{prepare_contract, IntegrationContext}, +// jar_contract_extensions::JarContractExtensions, +// product::RegisterProductCommand, +// }; +// +// #[tokio::test] +// #[mutants::skip] +// async fn jar_deletion() -> anyhow::Result<()> { +// println!("👷🏽 Run jar deletion test"); +// +// let mut context = prepare_contract(None, [RegisterProductCommand::Locked10Minutes60000Percents]).await?; +// +// let alice = context.alice().await?; +// +// context +// .sweat_jar() +// .create_jar( +// &alice, +// RegisterProductCommand::Locked10Minutes60000Percents.id(), +// 1_000_000, +// &context.ft_contract(), +// ) +// .await?; +// +// let jar_view = context +// .sweat_jar() +// .get_jars_for_account(alice.to_near()) +// .await? +// .into_iter() +// .next() +// .unwrap(); +// +// context.fast_forward_minutes(11).await?; +// +// let withdrawn_amount = context +// .sweat_jar() +// .withdraw(jar_view.id, None) +// .with_user(&alice) +// .await?; +// assert_eq!(withdrawn_amount.withdrawn_amount.0, 1_000_000); +// +// let alice_interest = context.sweat_jar().get_total_interest(alice.to_near()).await?; +// let claimed_amount = context +// .sweat_jar() +// .claim_total(None) +// .with_user(&alice) +// .await? +// .get_total() +// .0; +// assert_eq!(alice_interest.amount.total.0, claimed_amount); +// +// let alice_interest = context.sweat_jar().get_total_interest(alice.to_near()).await?; +// assert_eq!(alice_interest.amount.total.0, 0); +// +// let jars = context.sweat_jar().get_jars_for_account(alice.to_near()).await?; +// assert!(jars.is_empty()); +// +// Ok(()) +// } diff --git a/integration-tests/src/many_jars.rs b/integration-tests/src/many_jars.rs index 0e426d60..995165ab 100644 --- a/integration-tests/src/many_jars.rs +++ b/integration-tests/src/many_jars.rs @@ -1,159 +1,159 @@ -use anyhow::Result; -use nitka::{misc::ToNear, set_integration_logs_enabled}; -use sweat_jar_model::{ - api::{ClaimApiIntegration, IntegrationTestMethodsIntegration, JarApiIntegration, WithdrawApiIntegration}, - claimed_amount_view::ClaimedAmountView, - JAR_BATCH_SIZE, -}; - -use crate::{ - context::{prepare_contract, IntegrationContext}, - product::RegisterProductCommand::Locked5Minutes60000Percents, -}; - -#[tokio::test] -#[mutants::skip] -async fn claim_many_jars() -> Result<()> { - const INTEREST: u128 = 1_000; - - println!("👷🏽 Claim many jars test"); - - set_integration_logs_enabled(false); - - let mut context = prepare_contract(None, [Locked5Minutes60000Percents]).await?; - - let alice = context.alice().await?; - let manager = context.manager().await?; - - context - .sweat_jar() - .bulk_create_jars(alice.to_near(), Locked5Minutes60000Percents.id(), INTEREST, 2000) - .with_user(&manager) - .await?; - - assert_eq!( - context.sweat_jar().get_jars_for_account(alice.to_near()).await?.len(), - 2000 - ); - - context.fast_forward_minutes(5).await?; - - let claimed = context.sweat_jar().claim_total(true.into()).with_user(&alice).await?; - - let batch_claim_summ = claimed.get_total().0; - - dbg!(&batch_claim_summ); - - assert_eq!( - batch_claim_summ * 9, - context - .sweat_jar() - .get_total_interest(alice.to_near()) - .await? - .amount - .total - .0 - ); - - for i in 1..10 { - let claimed = context.sweat_jar().claim_total(true.into()).with_user(&alice).await?; - assert_eq!(claimed.get_total().0, batch_claim_summ); - - assert_eq!( - batch_claim_summ * (9 - i), - context - .sweat_jar() - .get_total_interest(alice.to_near()) - .await? - .amount - .total - .0 - ); - } - - assert_eq!( - context - .sweat_jar() - .get_total_interest(alice.to_near()) - .await? - .amount - .total - .0, - 0 - ); - - assert_eq!( - context.sweat_jar().get_jars_for_account(alice.to_near()).await?.len(), - 2000 - ); - - for i in 0..10 { - let withdrawn_summ = context.sweat_jar().withdraw_all(None).with_user(&alice).await?; - dbg!(&i); - assert_eq!(withdrawn_summ.jars.len(), JAR_BATCH_SIZE); - assert_eq!(withdrawn_summ.total_amount.0, INTEREST * JAR_BATCH_SIZE as u128); - } - - assert_eq!( - context.sweat_jar().get_jars_for_account(alice.to_near()).await?.len(), - 0 - ); - - Ok(()) -} - -#[tokio::test] -#[mutants::skip] -async fn restake_many_jars() -> Result<()> { - const INTEREST: u128 = 1_000; - const JARS_COUNT: u16 = 2000; - - println!("👷🏽 Restake many jars test"); - - set_integration_logs_enabled(false); - - let mut context = prepare_contract(None, [Locked5Minutes60000Percents]).await?; - - let alice = context.alice().await?; - let manager = context.manager().await?; - - context - .sweat_jar() - .bulk_create_jars(alice.to_near(), Locked5Minutes60000Percents.id(), INTEREST, JARS_COUNT) - .with_user(&manager) - .await?; - - assert_eq!( - context.sweat_jar().get_jars_for_account(alice.to_near()).await?.len(), - JARS_COUNT as usize - ); - - context.fast_forward_minutes(5).await?; - - for _ in 0..10 { - let ClaimedAmountView::Detailed(claimed) = - context.sweat_jar().claim_total(true.into()).with_user(&alice).await? - else { - panic!(); - }; - assert_eq!(claimed.detailed.len(), JAR_BATCH_SIZE); - - let restaked = context.sweat_jar().restake_all(None).with_user(&alice).await?; - assert_eq!(restaked.len(), JAR_BATCH_SIZE); - - assert_eq!( - context.sweat_jar().get_jars_for_account(alice.to_near()).await?.len(), - JARS_COUNT as usize - ); - } - - let jars = context.sweat_jar().get_jars_for_account(alice.to_near()).await?; - - let mut ids: Vec<_> = jars.iter().map(|j| j.id.0).collect(); - - ids.sort_unstable(); - - assert_eq!(ids, (2001..=4000).collect::>()); - - Ok(()) -} +// use anyhow::Result; +// use nitka::{misc::ToNear, set_integration_logs_enabled}; +// use sweat_jar_model::{ +// api::{ClaimApiIntegration, IntegrationTestMethodsIntegration, JarApiIntegration, WithdrawApiIntegration}, +// claimed_amount_view::ClaimedAmountView, +// JAR_BATCH_SIZE, +// }; +// +// use crate::{ +// context::{prepare_contract, IntegrationContext}, +// product::RegisterProductCommand::Locked5Minutes60000Percents, +// }; +// +// #[tokio::test] +// #[mutants::skip] +// async fn claim_many_jars() -> Result<()> { +// const INTEREST: u128 = 1_000; +// +// println!("👷🏽 Claim many jars test"); +// +// set_integration_logs_enabled(false); +// +// let mut context = prepare_contract(None, [Locked5Minutes60000Percents]).await?; +// +// let alice = context.alice().await?; +// let manager = context.manager().await?; +// +// context +// .sweat_jar() +// .bulk_create_jars(alice.to_near(), Locked5Minutes60000Percents.id(), INTEREST, 2000) +// .with_user(&manager) +// .await?; +// +// assert_eq!( +// context.sweat_jar().get_jars_for_account(alice.to_near()).await?.len(), +// 2000 +// ); +// +// context.fast_forward_minutes(5).await?; +// +// let claimed = context.sweat_jar().claim_total(true.into()).with_user(&alice).await?; +// +// let batch_claim_summ = claimed.get_total().0; +// +// dbg!(&batch_claim_summ); +// +// assert_eq!( +// batch_claim_summ * 9, +// context +// .sweat_jar() +// .get_total_interest(alice.to_near()) +// .await? +// .amount +// .total +// .0 +// ); +// +// for i in 1..10 { +// let claimed = context.sweat_jar().claim_total(true.into()).with_user(&alice).await?; +// assert_eq!(claimed.get_total().0, batch_claim_summ); +// +// assert_eq!( +// batch_claim_summ * (9 - i), +// context +// .sweat_jar() +// .get_total_interest(alice.to_near()) +// .await? +// .amount +// .total +// .0 +// ); +// } +// +// assert_eq!( +// context +// .sweat_jar() +// .get_total_interest(alice.to_near()) +// .await? +// .amount +// .total +// .0, +// 0 +// ); +// +// assert_eq!( +// context.sweat_jar().get_jars_for_account(alice.to_near()).await?.len(), +// 2000 +// ); +// +// for i in 0..10 { +// let withdrawn_summ = context.sweat_jar().withdraw_all(None).with_user(&alice).await?; +// dbg!(&i); +// assert_eq!(withdrawn_summ.jars.len(), JAR_BATCH_SIZE); +// assert_eq!(withdrawn_summ.total_amount.0, INTEREST * JAR_BATCH_SIZE as u128); +// } +// +// assert_eq!( +// context.sweat_jar().get_jars_for_account(alice.to_near()).await?.len(), +// 0 +// ); +// +// Ok(()) +// } +// +// #[tokio::test] +// #[mutants::skip] +// async fn restake_many_jars() -> Result<()> { +// const INTEREST: u128 = 1_000; +// const JARS_COUNT: u16 = 2000; +// +// println!("👷🏽 Restake many jars test"); +// +// set_integration_logs_enabled(false); +// +// let mut context = prepare_contract(None, [Locked5Minutes60000Percents]).await?; +// +// let alice = context.alice().await?; +// let manager = context.manager().await?; +// +// context +// .sweat_jar() +// .bulk_create_jars(alice.to_near(), Locked5Minutes60000Percents.id(), INTEREST, JARS_COUNT) +// .with_user(&manager) +// .await?; +// +// assert_eq!( +// context.sweat_jar().get_jars_for_account(alice.to_near()).await?.len(), +// JARS_COUNT as usize +// ); +// +// context.fast_forward_minutes(5).await?; +// +// for _ in 0..10 { +// let ClaimedAmountView::Detailed(claimed) = +// context.sweat_jar().claim_total(true.into()).with_user(&alice).await? +// else { +// panic!(); +// }; +// assert_eq!(claimed.detailed.len(), JAR_BATCH_SIZE); +// +// let restaked = context.sweat_jar().restake_all(None).with_user(&alice).await?; +// assert_eq!(restaked.len(), JAR_BATCH_SIZE); +// +// assert_eq!( +// context.sweat_jar().get_jars_for_account(alice.to_near()).await?.len(), +// JARS_COUNT as usize +// ); +// } +// +// let jars = context.sweat_jar().get_jars_for_account(alice.to_near()).await?; +// +// let mut ids: Vec<_> = jars.iter().map(|j| j.id.0).collect(); +// +// ids.sort_unstable(); +// +// assert_eq!(ids, (2001..=4000).collect::>()); +// +// Ok(()) +// } diff --git a/integration-tests/src/measure/after_withdraw.rs b/integration-tests/src/measure/after_withdraw.rs index 1503a8d8..a40e9f3d 100644 --- a/integration-tests/src/measure/after_withdraw.rs +++ b/integration-tests/src/measure/after_withdraw.rs @@ -1,75 +1,75 @@ -use anyhow::Result; -use near_workspaces::types::Gas; -use sweat_jar_model::{api::WithdrawApiIntegration, U32}; - -use crate::{ - context::{prepare_contract, IntegrationContext}, - jar_contract_extensions::JarContractExtensions, - measure::{measure::scoped_command_measure, utils::generate_permutations}, - product::RegisterProductCommand, -}; - -#[ignore] -#[tokio::test] -#[mutants::skip] -async fn measure_withdraw_test() -> Result<()> { - let result = scoped_command_measure( - generate_permutations( - &[ - RegisterProductCommand::Locked10Minutes6PercentsWithPercentWithdrawFee, - RegisterProductCommand::Locked10Minutes6PercentsWithFixedWithdrawFee, - ], - &[100_000, 200_000, 300_000, 400_000, 500_000], - ), - measure_one_withdraw, - ) - .await?; - - let all_gas: Vec<_> = result.into_iter().map(|res| res.1).collect(); - - dbg!(&all_gas); - - dbg!(all_gas.iter().max()); - dbg!(all_gas.iter().min()); - - Ok(()) -} - -#[ignore] -#[tokio::test] -#[mutants::skip] -async fn one_withdraw() -> anyhow::Result<()> { - let gas = measure_one_withdraw(( - RegisterProductCommand::Locked10Minutes6PercentsWithPercentWithdrawFee, - 100_000, - )) - .await?; - - dbg!(&gas); - - Ok(()) -} - -#[mutants::skip] -async fn measure_one_withdraw(data: (RegisterProductCommand, u128)) -> Result { - let (product, anmount) = data; - - let mut context = prepare_contract(None, [product]).await?; - - let alice = context.alice().await?; - - context - .sweat_jar() - .create_jar(&alice, product.id(), anmount, &context.ft_contract()) - .await?; - - context.fast_forward_hours(1).await?; - - Ok(context - .sweat_jar() - .withdraw(U32(0), None) - .with_user(&alice) - .result() - .await? - .total_gas_burnt) -} +// use anyhow::Result; +// use near_workspaces::types::Gas; +// use sweat_jar_model::{api::WithdrawApiIntegration, U32}; +// +// use crate::{ +// context::{prepare_contract, IntegrationContext}, +// jar_contract_extensions::JarContractExtensions, +// measure::{measure::scoped_command_measure, utils::generate_permutations}, +// product::RegisterProductCommand, +// }; +// +// #[ignore] +// #[tokio::test] +// #[mutants::skip] +// async fn measure_withdraw_test() -> Result<()> { +// let result = scoped_command_measure( +// generate_permutations( +// &[ +// RegisterProductCommand::Locked10Minutes6PercentsWithPercentWithdrawFee, +// RegisterProductCommand::Locked10Minutes6PercentsWithFixedWithdrawFee, +// ], +// &[100_000, 200_000, 300_000, 400_000, 500_000], +// ), +// measure_one_withdraw, +// ) +// .await?; +// +// let all_gas: Vec<_> = result.into_iter().map(|res| res.1).collect(); +// +// dbg!(&all_gas); +// +// dbg!(all_gas.iter().max()); +// dbg!(all_gas.iter().min()); +// +// Ok(()) +// } +// +// #[ignore] +// #[tokio::test] +// #[mutants::skip] +// async fn one_withdraw() -> anyhow::Result<()> { +// let gas = measure_one_withdraw(( +// RegisterProductCommand::Locked10Minutes6PercentsWithPercentWithdrawFee, +// 100_000, +// )) +// .await?; +// +// dbg!(&gas); +// +// Ok(()) +// } +// +// #[mutants::skip] +// async fn measure_one_withdraw(data: (RegisterProductCommand, u128)) -> Result { +// let (product, anmount) = data; +// +// let mut context = prepare_contract(None, [product]).await?; +// +// let alice = context.alice().await?; +// +// context +// .sweat_jar() +// .create_jar(&alice, product.id(), anmount, &context.ft_contract()) +// .await?; +// +// context.fast_forward_hours(1).await?; +// +// Ok(context +// .sweat_jar() +// .withdraw(U32(0), None) +// .with_user(&alice) +// .result() +// .await? +// .total_gas_burnt) +// } diff --git a/integration-tests/src/measure/batch_penalty.rs b/integration-tests/src/measure/batch_penalty.rs index 45effd4c..723750ba 100644 --- a/integration-tests/src/measure/batch_penalty.rs +++ b/integration-tests/src/measure/batch_penalty.rs @@ -1,94 +1,94 @@ -use std::collections::HashMap; - -use anyhow::Result; -use near_workspaces::types::Gas; -use nitka::misc::ToNear; -use sweat_jar_model::api::{JarApiIntegration, PenaltyApiIntegration}; - -use crate::{ - context::{prepare_contract, IntegrationContext}, - measure::{ - measure::scoped_command_measure, - utils::{add_jar, append_measure, generate_permutations, measure_jars_range, retry_until_ok, MeasureData}, - }, - product::RegisterProductCommand, -}; - -#[ignore] -#[tokio::test] -async fn measure_batch_penalty_test() -> Result<()> { - async fn batch_penalty() -> Result<()> { - let measured = scoped_command_measure( - generate_permutations( - &[RegisterProductCommand::Flexible6Months6Percents], - &measure_jars_range(), - ), - measure_batch_penalty, - ) - .await?; - - let mut map: HashMap> = HashMap::new(); - - for measure in measured { - map.entry(measure.0 .0).or_default().push((measure.1, measure.0 .1)); - } - - let map: HashMap = map - .into_iter() - .map(|(key, gas_cost)| { - let mut differences: Vec = Vec::new(); - for i in 1..gas_cost.len() { - let diff = gas_cost[i].0.as_gas() as i128 - gas_cost[i - 1].0.as_gas() as i128; - differences.push(diff); - } - - (key, MeasureData::new(gas_cost, differences)) - }) - .collect(); - - append_measure("batch_penalty", map) - } - - retry_until_ok(batch_penalty).await?; - - Ok(()) -} - -#[ignore] -#[tokio::test] -async fn single_batch_penalty() -> Result<()> { - let gas = measure_batch_penalty((RegisterProductCommand::Flexible6Months6Percents, 1)).await?; - - dbg!(&gas); - - Ok(()) -} - -async fn measure_batch_penalty(input: (RegisterProductCommand, usize)) -> Result { - let (product, jars_count) = input; - - let mut context = prepare_contract(None, [product]).await?; - - let alice = context.alice().await?; - let manager = context.manager().await?; - - for _ in 0..jars_count { - add_jar(&context, &alice, product, 100_000).await?; - } - - let jars = context - .sweat_jar() - .get_jars_for_account(alice.to_near()) - .await? - .into_iter() - .map(|j| j.id) - .collect(); - - Ok(context - .sweat_jar() - .batch_set_penalty(vec![(alice.to_near(), jars)], true) - .with_user(&manager) - .result() - .await? - .total_gas_burnt) -} +// use std::collections::HashMap; +// +// use anyhow::Result; +// use near_workspaces::types::Gas; +// use nitka::misc::ToNear; +// use sweat_jar_model::api::{JarApiIntegration, PenaltyApiIntegration}; +// +// use crate::{ +// context::{prepare_contract, IntegrationContext}, +// measure::{ +// measure::scoped_command_measure, +// utils::{add_jar, append_measure, generate_permutations, measure_jars_range, retry_until_ok, MeasureData}, +// }, +// product::RegisterProductCommand, +// }; +// +// #[ignore] +// #[tokio::test] +// async fn measure_batch_penalty_test() -> Result<()> { +// async fn batch_penalty() -> Result<()> { +// let measured = scoped_command_measure( +// generate_permutations( +// &[RegisterProductCommand::Flexible6Months6Percents], +// &measure_jars_range(), +// ), +// measure_batch_penalty, +// ) +// .await?; +// +// let mut map: HashMap> = HashMap::new(); +// +// for measure in measured { +// map.entry(measure.0 .0).or_default().push((measure.1, measure.0 .1)); +// } +// +// let map: HashMap = map +// .into_iter() +// .map(|(key, gas_cost)| { +// let mut differences: Vec = Vec::new(); +// for i in 1..gas_cost.len() { +// let diff = gas_cost[i].0.as_gas() as i128 - gas_cost[i - 1].0.as_gas() as i128; +// differences.push(diff); +// } +// +// (key, MeasureData::new(gas_cost, differences)) +// }) +// .collect(); +// +// append_measure("batch_penalty", map) +// } +// +// retry_until_ok(batch_penalty).await?; +// +// Ok(()) +// } +// +// #[ignore] +// #[tokio::test] +// async fn single_batch_penalty() -> Result<()> { +// let gas = measure_batch_penalty((RegisterProductCommand::Flexible6Months6Percents, 1)).await?; +// +// dbg!(&gas); +// +// Ok(()) +// } +// +// async fn measure_batch_penalty(input: (RegisterProductCommand, usize)) -> Result { +// let (product, jars_count) = input; +// +// let mut context = prepare_contract(None, [product]).await?; +// +// let alice = context.alice().await?; +// let manager = context.manager().await?; +// +// for _ in 0..jars_count { +// add_jar(&context, &alice, product, 100_000).await?; +// } +// +// let jars = context +// .sweat_jar() +// .get_jars_for_account(alice.to_near()) +// .await? +// .into_iter() +// .map(|j| j.id) +// .collect(); +// +// Ok(context +// .sweat_jar() +// .batch_set_penalty(vec![(alice.to_near(), jars)], true) +// .with_user(&manager) +// .result() +// .await? +// .total_gas_burnt) +// } diff --git a/integration-tests/src/measure/restake.rs b/integration-tests/src/measure/restake.rs index 79a7e6ab..5ac92731 100644 --- a/integration-tests/src/measure/restake.rs +++ b/integration-tests/src/measure/restake.rs @@ -1,93 +1,93 @@ -use std::collections::HashMap; - -use anyhow::Result; -use near_workspaces::types::Gas; -use nitka::misc::ToNear; -use sweat_jar_model::api::JarApiIntegration; - -use crate::{ - context::{prepare_contract, IntegrationContext}, - measure::{ - measure::scoped_command_measure, - random_element::RandomElement, - utils::{add_jar, append_measure, generate_permutations, measure_jars_range, retry_until_ok, MeasureData}, - }, - product::RegisterProductCommand, -}; - -#[ignore] -#[tokio::test] -#[mutants::skip] -async fn measure_restake_total_test() -> Result<()> { - async fn restake() -> Result<()> { - let measured = scoped_command_measure( - generate_permutations( - &[RegisterProductCommand::Locked10Minutes6Percents], - &measure_jars_range(), - ), - measure_restake, - ) - .await?; - - let mut map: HashMap> = HashMap::new(); - - for measure in measured { - map.entry(measure.0 .0).or_default().push((measure.1, measure.0 .1)); - } - - let map: HashMap = map - .into_iter() - .map(|(key, gas_cost)| { - let mut differences: Vec = Vec::new(); - for i in 1..gas_cost.len() { - let diff = gas_cost[i].0.as_gas() as i128 - gas_cost[i - 1].0.as_gas() as i128; - differences.push(diff); - } - - (key, MeasureData::new(gas_cost, differences)) - }) - .collect(); - - append_measure("restake", map) - } - - retry_until_ok(restake).await?; - - Ok(()) -} - -#[ignore] -#[tokio::test] -#[mutants::skip] -async fn one_restake() -> anyhow::Result<()> { - let gas = measure_restake((RegisterProductCommand::Locked10Minutes6Percents, 1)).await?; - - dbg!(&gas); - - Ok(()) -} - -#[mutants::skip] -pub(crate) async fn measure_restake(input: (RegisterProductCommand, usize)) -> anyhow::Result { - let (product, jars_count) = input; - - let mut context = prepare_contract(None, [product]).await?; - - let alice = context.alice().await?; - - for _ in 0..jars_count { - add_jar(&context, &alice, product, 100_000).await?; - } - - context.fast_forward_hours(2).await?; - - let jars = context.sweat_jar().get_jars_for_account(alice.to_near()).await?; - - Ok(context - .sweat_jar() - .restake(jars.random_element().id) - .with_user(&alice) - .result() - .await? - .total_gas_burnt) -} +// use std::collections::HashMap; +// +// use anyhow::Result; +// use near_workspaces::types::Gas; +// use nitka::misc::ToNear; +// use sweat_jar_model::api::JarApiIntegration; +// +// use crate::{ +// context::{prepare_contract, IntegrationContext}, +// measure::{ +// measure::scoped_command_measure, +// random_element::RandomElement, +// utils::{add_jar, append_measure, generate_permutations, measure_jars_range, retry_until_ok, MeasureData}, +// }, +// product::RegisterProductCommand, +// }; +// +// #[ignore] +// #[tokio::test] +// #[mutants::skip] +// async fn measure_restake_total_test() -> Result<()> { +// async fn restake() -> Result<()> { +// let measured = scoped_command_measure( +// generate_permutations( +// &[RegisterProductCommand::Locked10Minutes6Percents], +// &measure_jars_range(), +// ), +// measure_restake, +// ) +// .await?; +// +// let mut map: HashMap> = HashMap::new(); +// +// for measure in measured { +// map.entry(measure.0 .0).or_default().push((measure.1, measure.0 .1)); +// } +// +// let map: HashMap = map +// .into_iter() +// .map(|(key, gas_cost)| { +// let mut differences: Vec = Vec::new(); +// for i in 1..gas_cost.len() { +// let diff = gas_cost[i].0.as_gas() as i128 - gas_cost[i - 1].0.as_gas() as i128; +// differences.push(diff); +// } +// +// (key, MeasureData::new(gas_cost, differences)) +// }) +// .collect(); +// +// append_measure("restake", map) +// } +// +// retry_until_ok(restake).await?; +// +// Ok(()) +// } +// +// #[ignore] +// #[tokio::test] +// #[mutants::skip] +// async fn one_restake() -> anyhow::Result<()> { +// let gas = measure_restake((RegisterProductCommand::Locked10Minutes6Percents, 1)).await?; +// +// dbg!(&gas); +// +// Ok(()) +// } +// +// #[mutants::skip] +// pub(crate) async fn measure_restake(input: (RegisterProductCommand, usize)) -> anyhow::Result { +// let (product, jars_count) = input; +// +// let mut context = prepare_contract(None, [product]).await?; +// +// let alice = context.alice().await?; +// +// for _ in 0..jars_count { +// add_jar(&context, &alice, product, 100_000).await?; +// } +// +// context.fast_forward_hours(2).await?; +// +// let jars = context.sweat_jar().get_jars_for_account(alice.to_near()).await?; +// +// Ok(context +// .sweat_jar() +// .restake(jars.random_element().id) +// .with_user(&alice) +// .result() +// .await? +// .total_gas_burnt) +// } diff --git a/integration-tests/src/measure/withdraw.rs b/integration-tests/src/measure/withdraw.rs index 2d8eea81..35c43a1e 100644 --- a/integration-tests/src/measure/withdraw.rs +++ b/integration-tests/src/measure/withdraw.rs @@ -1,99 +1,99 @@ -use std::collections::HashMap; - -use anyhow::Result; -use near_workspaces::types::Gas; -use nitka::misc::ToNear; -use sweat_jar_model::api::{JarApiIntegration, WithdrawApiIntegration}; - -use crate::{ - context::{prepare_contract, IntegrationContext}, - measure::{ - measure::scoped_command_measure, - random_element::RandomElement, - utils::{add_jar, append_measure, generate_permutations, measure_jars_range, retry_until_ok, MeasureData}, - }, - product::RegisterProductCommand, -}; - -#[ignore] -#[tokio::test] -#[mutants::skip] -async fn measure_withdraw_total_test() -> Result<()> { - async fn withdraw() -> Result<()> { - let measured = scoped_command_measure( - generate_permutations( - &[ - RegisterProductCommand::Locked10Minutes6Percents, - RegisterProductCommand::Locked10Minutes6PercentsWithFixedWithdrawFee, - RegisterProductCommand::Locked10Minutes6PercentsWithPercentWithdrawFee, - ], - &measure_jars_range(), - ), - measure_withdraw, - ) - .await?; - - let mut map: HashMap> = HashMap::new(); - - for measure in measured { - map.entry(measure.0 .0).or_default().push((measure.1, measure.0 .1)); - } - - let map: HashMap = map - .into_iter() - .map(|(key, gas_cost)| { - let mut differences: Vec = Vec::new(); - for i in 1..gas_cost.len() { - let diff = gas_cost[i].0.as_gas() as i128 - gas_cost[i - 1].0.as_gas() as i128; - differences.push(diff); - } - - (key, MeasureData::new(gas_cost, differences)) - }) - .collect(); - - append_measure("withdraw", map) - } - - retry_until_ok(withdraw).await?; - - Ok(()) -} - -#[ignore] -#[tokio::test] -#[mutants::skip] -async fn one_withdraw() -> anyhow::Result<()> { - let gas = measure_withdraw((RegisterProductCommand::Locked10Minutes6Percents, 1)).await?; - - dbg!(&gas); - - Ok(()) -} - -#[mutants::skip] -async fn measure_withdraw(input: (RegisterProductCommand, usize)) -> anyhow::Result { - let (product, jars_count) = input; - - let mut context = prepare_contract(None, [product]).await?; - - let alice = context.alice().await?; - - for _ in 0..jars_count { - add_jar(&context, &alice, product, 100_000).await?; - } - - context.fast_forward_hours(2).await?; - - let jars = context.sweat_jar().get_jars_for_account(alice.to_near()).await?; - - let jar = jars.random_element(); - - Ok(context - .sweat_jar() - .withdraw(jar.id, None) - .with_user(&alice) - .result() - .await? - .total_gas_burnt) -} +// use std::collections::HashMap; +// +// use anyhow::Result; +// use near_workspaces::types::Gas; +// use nitka::misc::ToNear; +// use sweat_jar_model::api::{JarApiIntegration, WithdrawApiIntegration}; +// +// use crate::{ +// context::{prepare_contract, IntegrationContext}, +// measure::{ +// measure::scoped_command_measure, +// random_element::RandomElement, +// utils::{add_jar, append_measure, generate_permutations, measure_jars_range, retry_until_ok, MeasureData}, +// }, +// product::RegisterProductCommand, +// }; +// +// #[ignore] +// #[tokio::test] +// #[mutants::skip] +// async fn measure_withdraw_total_test() -> Result<()> { +// async fn withdraw() -> Result<()> { +// let measured = scoped_command_measure( +// generate_permutations( +// &[ +// RegisterProductCommand::Locked10Minutes6Percents, +// RegisterProductCommand::Locked10Minutes6PercentsWithFixedWithdrawFee, +// RegisterProductCommand::Locked10Minutes6PercentsWithPercentWithdrawFee, +// ], +// &measure_jars_range(), +// ), +// measure_withdraw, +// ) +// .await?; +// +// let mut map: HashMap> = HashMap::new(); +// +// for measure in measured { +// map.entry(measure.0 .0).or_default().push((measure.1, measure.0 .1)); +// } +// +// let map: HashMap = map +// .into_iter() +// .map(|(key, gas_cost)| { +// let mut differences: Vec = Vec::new(); +// for i in 1..gas_cost.len() { +// let diff = gas_cost[i].0.as_gas() as i128 - gas_cost[i - 1].0.as_gas() as i128; +// differences.push(diff); +// } +// +// (key, MeasureData::new(gas_cost, differences)) +// }) +// .collect(); +// +// append_measure("withdraw", map) +// } +// +// retry_until_ok(withdraw).await?; +// +// Ok(()) +// } +// +// #[ignore] +// #[tokio::test] +// #[mutants::skip] +// async fn one_withdraw() -> anyhow::Result<()> { +// let gas = measure_withdraw((RegisterProductCommand::Locked10Minutes6Percents, 1)).await?; +// +// dbg!(&gas); +// +// Ok(()) +// } +// +// #[mutants::skip] +// async fn measure_withdraw(input: (RegisterProductCommand, usize)) -> anyhow::Result { +// let (product, jars_count) = input; +// +// let mut context = prepare_contract(None, [product]).await?; +// +// let alice = context.alice().await?; +// +// for _ in 0..jars_count { +// add_jar(&context, &alice, product, 100_000).await?; +// } +// +// context.fast_forward_hours(2).await?; +// +// let jars = context.sweat_jar().get_jars_for_account(alice.to_near()).await?; +// +// let jar = jars.random_element(); +// +// Ok(context +// .sweat_jar() +// .withdraw(jar.id, None) +// .with_user(&alice) +// .result() +// .await? +// .total_gas_burnt) +// } diff --git a/integration-tests/src/measure/withdraw_all.rs b/integration-tests/src/measure/withdraw_all.rs index 9a4d482e..55aec1ce 100644 --- a/integration-tests/src/measure/withdraw_all.rs +++ b/integration-tests/src/measure/withdraw_all.rs @@ -1,43 +1,43 @@ -use anyhow::Result; -use nitka::{measure::utils::pretty_gas_string, set_integration_logs_enabled}; -use sweat_jar_model::api::{ClaimApiIntegration, WithdrawApiIntegration}; - -use crate::{ - context::{prepare_contract, IntegrationContext}, - measure::utils::add_jar, - product::RegisterProductCommand, -}; - -#[ignore] -#[tokio::test] -#[mutants::skip] -async fn measure_withdraw_all() -> Result<()> { - set_integration_logs_enabled(false); - - let product = RegisterProductCommand::Locked5Minutes60000Percents; - let mut context = prepare_contract(None, [product]).await?; - let alice = context.alice().await?; - - for _ in 0..200 { - add_jar(&context, &alice, product, 10_000).await?; - } - - context.fast_forward_minutes(6).await?; - - context.sweat_jar().claim_total(None).with_user(&alice).await?; - - let gas = context - .sweat_jar() - .withdraw_all(None) - .with_user(&alice) - .result() - .await? - .total_gas_burnt; - dbg!(pretty_gas_string(gas)); - - // 1 jar - 18 TGas 208 GGas total: 18208042945131 - // 100 jars - 65 TGas 547 GGas total: 65547362403008 - // 200 jars - 111 TGas 935 GGas total: 111935634284610 - - Ok(()) -} +// use anyhow::Result; +// use nitka::{measure::utils::pretty_gas_string, set_integration_logs_enabled}; +// use sweat_jar_model::api::{ClaimApiIntegration, WithdrawApiIntegration}; +// +// use crate::{ +// context::{prepare_contract, IntegrationContext}, +// measure::utils::add_jar, +// product::RegisterProductCommand, +// }; +// +// #[ignore] +// #[tokio::test] +// #[mutants::skip] +// async fn measure_withdraw_all() -> Result<()> { +// set_integration_logs_enabled(false); +// +// let product = RegisterProductCommand::Locked5Minutes60000Percents; +// let mut context = prepare_contract(None, [product]).await?; +// let alice = context.alice().await?; +// +// for _ in 0..200 { +// add_jar(&context, &alice, product, 10_000).await?; +// } +// +// context.fast_forward_minutes(6).await?; +// +// context.sweat_jar().claim_total(None).with_user(&alice).await?; +// +// let gas = context +// .sweat_jar() +// .withdraw_all(None) +// .with_user(&alice) +// .result() +// .await? +// .total_gas_burnt; +// dbg!(pretty_gas_string(gas)); +// +// // 1 jar - 18 TGas 208 GGas total: 18208042945131 +// // 100 jars - 65 TGas 547 GGas total: 65547362403008 +// // 200 jars - 111 TGas 935 GGas total: 111935634284610 +// +// Ok(()) +// } diff --git a/integration-tests/src/migrations/defi.rs b/integration-tests/src/migrations/defi.rs index 1ed84b05..100a6294 100644 --- a/integration-tests/src/migrations/defi.rs +++ b/integration-tests/src/migrations/defi.rs @@ -1,131 +1,131 @@ -use near_workspaces::types::NearToken; -use nitka::{misc::ToNear, near_sdk::serde_json::json}; -use sweat_jar_model::api::{InitApiIntegration, JarApiIntegration, ProductApiIntegration}; -use sweat_model::{FungibleTokenCoreIntegration, StorageManagementIntegration, SweatApiIntegration}; - -use crate::{ - context::{Context, IntegrationContext, FT_CONTRACT, SWEAT_JAR}, - product::RegisterProductCommand, -}; - -#[tokio::test] -#[mutants::skip] -async fn defi_migration() -> anyhow::Result<()> { - println!("👷🏽 Run migration test"); - - let mut context = Context::new(&[FT_CONTRACT, SWEAT_JAR], true, "build-integration".into()).await?; - - let manager = &context.account("manager").await?; - let alice = &context.account("alice").await?; - let bob = &context.account("bob").await?; - let fee_account = &context.account("fee").await?; - - context.ft_contract().new(".u.sweat.testnet".to_string().into()).await?; - context - .sweat_jar() - .init( - context.ft_contract().contract.as_account().to_near(), - fee_account.to_near(), - manager.to_near(), - ) - .await?; - - context - .ft_contract() - .storage_deposit(context.sweat_jar().contract.as_account().to_near().into(), None) - .await?; - - context - .ft_contract() - .storage_deposit(manager.to_near().into(), None) - .await?; - context - .ft_contract() - .storage_deposit(alice.to_near().into(), None) - .await?; - context - .ft_contract() - .storage_deposit(bob.to_near().into(), None) - .await?; - - context - .ft_contract() - .tge_mint(&manager.to_near(), 3_000_000.into()) - .await?; - context - .ft_contract() - .tge_mint(&alice.to_near(), 100_000_000.into()) - .await?; - context - .ft_contract() - .tge_mint(&bob.to_near(), 100_000_000_000.into()) - .await?; - - context - .sweat_jar() - .register_product(RegisterProductCommand::Locked12Months12Percents.get()) - .with_user(&manager) - .await?; - - context.fast_forward_hours(1).await?; - - context - .ft_contract() - .ft_transfer_call( - context.sweat_jar().contract.as_account().to_near(), - 3_000_000.into(), - None, - json!({ - "type": "migrate", - "data": [ - { - "id": "old_0", - "account_id": alice.id(), - "product_id": RegisterProductCommand::Locked12Months12Percents.id(), - "principal": "2000000", - "created_at": "0", - }, - { - "id": "old_1", - "account_id": alice.id(), - "product_id": RegisterProductCommand::Locked12Months12Percents.id(), - "principal": "700000", - "created_at": "100", - }, - { - "id": "old_2", - "account_id": bob.id(), - "product_id": RegisterProductCommand::Locked12Months12Percents.id(), - "principal": "300000", - "created_at": "0", - }, - ] - }) - .to_string(), - ) - .deposit(NearToken::from_yoctonear(1)) - .with_user(&manager) - .await?; - - let manager_balance = context.ft_contract().ft_balance_of(manager.to_near()).await?; - assert_eq!(0, manager_balance.0); - - let alice_jars = context.sweat_jar().get_jars_for_account(alice.to_near()).await?; - assert_eq!(2, alice_jars.len()); - - let alice_first_jar = alice_jars.first().unwrap(); - assert_eq!(1, alice_first_jar.id.0); - assert_eq!(2000000, alice_first_jar.principal.0); - - let alice_second_jar = alice_jars.get(1).unwrap(); - assert_eq!(2, alice_second_jar.id.0); - assert_eq!(700000, alice_second_jar.principal.0); - - let alice_principal = context.sweat_jar().get_total_principal(alice.to_near()).await?; - assert_eq!(2_700_000, alice_principal.total.0); - - let bob_principal = context.sweat_jar().get_total_principal(bob.to_near()).await?; - assert_eq!(300_000, bob_principal.total.0); - - Ok(()) -} +// use near_workspaces::types::NearToken; +// use nitka::{misc::ToNear, near_sdk::serde_json::json}; +// use sweat_jar_model::api::{InitApiIntegration, JarApiIntegration, ProductApiIntegration}; +// use sweat_model::{FungibleTokenCoreIntegration, StorageManagementIntegration, SweatApiIntegration}; +// +// use crate::{ +// context::{Context, IntegrationContext, FT_CONTRACT, SWEAT_JAR}, +// product::RegisterProductCommand, +// }; +// +// #[tokio::test] +// #[mutants::skip] +// async fn defi_migration() -> anyhow::Result<()> { +// println!("👷🏽 Run migration test"); +// +// let mut context = Context::new(&[FT_CONTRACT, SWEAT_JAR], true, "build-integration".into()).await?; +// +// let manager = &context.account("manager").await?; +// let alice = &context.account("alice").await?; +// let bob = &context.account("bob").await?; +// let fee_account = &context.account("fee").await?; +// +// context.ft_contract().new(".u.sweat.testnet".to_string().into()).await?; +// context +// .sweat_jar() +// .init( +// context.ft_contract().contract.as_account().to_near(), +// fee_account.to_near(), +// manager.to_near(), +// ) +// .await?; +// +// context +// .ft_contract() +// .storage_deposit(context.sweat_jar().contract.as_account().to_near().into(), None) +// .await?; +// +// context +// .ft_contract() +// .storage_deposit(manager.to_near().into(), None) +// .await?; +// context +// .ft_contract() +// .storage_deposit(alice.to_near().into(), None) +// .await?; +// context +// .ft_contract() +// .storage_deposit(bob.to_near().into(), None) +// .await?; +// +// context +// .ft_contract() +// .tge_mint(&manager.to_near(), 3_000_000.into()) +// .await?; +// context +// .ft_contract() +// .tge_mint(&alice.to_near(), 100_000_000.into()) +// .await?; +// context +// .ft_contract() +// .tge_mint(&bob.to_near(), 100_000_000_000.into()) +// .await?; +// +// context +// .sweat_jar() +// .register_product(RegisterProductCommand::Locked12Months12Percents.get()) +// .with_user(&manager) +// .await?; +// +// context.fast_forward_hours(1).await?; +// +// context +// .ft_contract() +// .ft_transfer_call( +// context.sweat_jar().contract.as_account().to_near(), +// 3_000_000.into(), +// None, +// json!({ +// "type": "migrate", +// "data": [ +// { +// "id": "old_0", +// "account_id": alice.id(), +// "product_id": RegisterProductCommand::Locked12Months12Percents.id(), +// "principal": "2000000", +// "created_at": "0", +// }, +// { +// "id": "old_1", +// "account_id": alice.id(), +// "product_id": RegisterProductCommand::Locked12Months12Percents.id(), +// "principal": "700000", +// "created_at": "100", +// }, +// { +// "id": "old_2", +// "account_id": bob.id(), +// "product_id": RegisterProductCommand::Locked12Months12Percents.id(), +// "principal": "300000", +// "created_at": "0", +// }, +// ] +// }) +// .to_string(), +// ) +// .deposit(NearToken::from_yoctonear(1)) +// .with_user(&manager) +// .await?; +// +// let manager_balance = context.ft_contract().ft_balance_of(manager.to_near()).await?; +// assert_eq!(0, manager_balance.0); +// +// let alice_jars = context.sweat_jar().get_jars_for_account(alice.to_near()).await?; +// assert_eq!(2, alice_jars.len()); +// +// let alice_first_jar = alice_jars.first().unwrap(); +// assert_eq!(1, alice_first_jar.id.0); +// assert_eq!(2000000, alice_first_jar.principal.0); +// +// let alice_second_jar = alice_jars.get(1).unwrap(); +// assert_eq!(2, alice_second_jar.id.0); +// assert_eq!(700000, alice_second_jar.principal.0); +// +// let alice_principal = context.sweat_jar().get_total_principal(alice.to_near()).await?; +// assert_eq!(2_700_000, alice_principal.total.0); +// +// let bob_principal = context.sweat_jar().get_total_principal(bob.to_near()).await?; +// assert_eq!(300_000, bob_principal.total.0); +// +// Ok(()) +// } diff --git a/integration-tests/src/migrations/score_jars.rs b/integration-tests/src/migrations/score_jars.rs index a37deda7..3dedc5f7 100644 --- a/integration-tests/src/migrations/score_jars.rs +++ b/integration-tests/src/migrations/score_jars.rs @@ -1,131 +1,131 @@ -use anyhow::Result; -use nitka::{build::build_contract, misc::ToNear, near_sdk::json_types::U128}; -use sweat_jar_model::api::{ - InitApiIntegration, JarApiIntegration, MigrationToStepJarsIntegration, ProductApiIntegration, SweatJarContract, -}; -use sweat_model::{FungibleTokenCoreIntegration, StorageManagementIntegration, SweatApiIntegration, SweatContract}; - -use crate::{ - jar_contract_extensions::JarContractExtensions, migrations::helpers::load_wasm, product::RegisterProductCommand, -}; - -#[tokio::test] -async fn migrate_to_score_jars() -> Result<()> { - build_contract("build-integration".into())?; - - let ft_code = load_wasm("res/sweat.wasm"); - let jar_old_code = load_wasm("res_test/sweat_jar_pre_score_jars.wasm"); - let jar_new_code = load_wasm("res/sweat_jar.wasm"); - - let worker = near_workspaces::sandbox().await?; - - let fee_account = worker.dev_create_account().await?; - let manager_account = worker.dev_create_account().await?; - let bob = worker.dev_create_account().await?; - - let ft_account = worker.dev_create_account().await?; - let ft_contract = ft_account.deploy(&ft_code).await?.into_result()?; - let ft_contract = SweatContract { contract: &ft_contract }; - - ft_contract.new(".u.sweat.testnet".to_string().into()).await?; - - let jar_account = worker.dev_create_account().await?; - let old_jar_contract = jar_account.deploy(&jar_old_code).await?.into_result()?; - let old_jar_contract = SweatJarContract { - contract: &old_jar_contract, - }; - - ft_contract.storage_deposit(jar_account.to_near().into(), None).await?; - ft_contract.tge_mint(&jar_account.to_near(), U128(100_000_000)).await?; - - old_jar_contract - .init(ft_account.to_near(), fee_account.to_near(), manager_account.to_near()) - .await?; - - ft_contract.tge_mint(&bob.to_near(), 1_000_000.into()).await?; - - for product in RegisterProductCommand::all() { - if product.id() == RegisterProductCommand::Locked6Months6Percents.get().id { - continue; - } - - old_jar_contract - .register_product(product.get()) - .with_user(&manager_account) - .await?; - } - - let products_old = old_jar_contract.get_products().with_user(&ft_account).await?; - assert_eq!(products_old.len(), 9); - - let bob_jars = old_jar_contract.get_jars_for_account(bob.to_near()).await?; - assert!(bob_jars.is_empty()); - - let staked = old_jar_contract - .create_jar( - &bob, - RegisterProductCommand::Locked10Minutes6PercentsTopUp.get().id, - 100_000, - &ft_contract, - ) - .await?; - - let bob_jars_old = old_jar_contract.get_jars_for_account(bob.to_near()).await?; - - assert_eq!(bob_jars_old.len(), 1); - - assert_eq!(staked.0, 100_000); - - assert_eq!(ft_contract.ft_balance_of(bob.to_near()).await?.0, 900_000); - - drop(old_jar_contract); - - let new_jar_contract = jar_account.deploy(&jar_new_code).await?.into_result()?; - let new_jar_contract = SweatJarContract { - contract: &new_jar_contract, - }; - - new_jar_contract.migrate_state_to_step_jars().await?; - - let products_new = new_jar_contract.get_products().with_user(&ft_account).await?; - assert_eq!(products_old, products_new); - - let bob_jars_new = new_jar_contract.get_jars_for_account(bob.to_near()).await?; - assert_eq!(bob_jars_old, bob_jars_new); - - new_jar_contract - .register_product(RegisterProductCommand::Locked6Months6Percents.get()) - .with_user(&manager_account) - .await?; - - let products = new_jar_contract.get_products().with_user(&ft_account).await?; - assert!(products.iter().all(|a| a.score_cap == 0)); - assert_eq!(products.len(), 10); - - let staked = new_jar_contract - .create_jar( - &bob, - RegisterProductCommand::Locked10Minutes6PercentsTopUp.get().id, - 100_000, - &ft_contract, - ) - .await?; - - let bob_jars = new_jar_contract.get_jars_for_account(bob.to_near()).await?; - - assert_eq!(bob_jars.len(), 2); - - assert_eq!(staked.0, 100_000); - - assert_eq!(ft_contract.ft_balance_of(bob.to_near()).await?.0, 800_000); - - new_jar_contract - .register_product(RegisterProductCommand::Locked10Minutes20000ScoreCap.get()) - .with_user(&manager_account) - .await?; - - let products = new_jar_contract.get_products().with_user(&ft_account).await?; - assert_eq!(products.last().unwrap().score_cap, 20_000); - - Ok(()) -} +// use anyhow::Result; +// use nitka::{build::build_contract, misc::ToNear, near_sdk::json_types::U128}; +// use sweat_jar_model::api::{ +// InitApiIntegration, JarApiIntegration, MigrationToStepJarsIntegration, ProductApiIntegration, SweatJarContract, +// }; +// use sweat_model::{FungibleTokenCoreIntegration, StorageManagementIntegration, SweatApiIntegration, SweatContract}; +// +// use crate::{ +// jar_contract_extensions::JarContractExtensions, migrations::helpers::load_wasm, product::RegisterProductCommand, +// }; +// +// #[tokio::test] +// async fn migrate_to_score_jars() -> Result<()> { +// build_contract("build-integration".into())?; +// +// let ft_code = load_wasm("res/sweat.wasm"); +// let jar_old_code = load_wasm("res_test/sweat_jar_pre_score_jars.wasm"); +// let jar_new_code = load_wasm("res/sweat_jar.wasm"); +// +// let worker = near_workspaces::sandbox().await?; +// +// let fee_account = worker.dev_create_account().await?; +// let manager_account = worker.dev_create_account().await?; +// let bob = worker.dev_create_account().await?; +// +// let ft_account = worker.dev_create_account().await?; +// let ft_contract = ft_account.deploy(&ft_code).await?.into_result()?; +// let ft_contract = SweatContract { contract: &ft_contract }; +// +// ft_contract.new(".u.sweat.testnet".to_string().into()).await?; +// +// let jar_account = worker.dev_create_account().await?; +// let old_jar_contract = jar_account.deploy(&jar_old_code).await?.into_result()?; +// let old_jar_contract = SweatJarContract { +// contract: &old_jar_contract, +// }; +// +// ft_contract.storage_deposit(jar_account.to_near().into(), None).await?; +// ft_contract.tge_mint(&jar_account.to_near(), U128(100_000_000)).await?; +// +// old_jar_contract +// .init(ft_account.to_near(), fee_account.to_near(), manager_account.to_near()) +// .await?; +// +// ft_contract.tge_mint(&bob.to_near(), 1_000_000.into()).await?; +// +// for product in RegisterProductCommand::all() { +// if product.id() == RegisterProductCommand::Locked6Months6Percents.get().id { +// continue; +// } +// +// old_jar_contract +// .register_product(product.get()) +// .with_user(&manager_account) +// .await?; +// } +// +// let products_old = old_jar_contract.get_products().with_user(&ft_account).await?; +// assert_eq!(products_old.len(), 9); +// +// let bob_jars = old_jar_contract.get_jars_for_account(bob.to_near()).await?; +// assert!(bob_jars.is_empty()); +// +// let staked = old_jar_contract +// .create_jar( +// &bob, +// RegisterProductCommand::Locked10Minutes6PercentsTopUp.get().id, +// 100_000, +// &ft_contract, +// ) +// .await?; +// +// let bob_jars_old = old_jar_contract.get_jars_for_account(bob.to_near()).await?; +// +// assert_eq!(bob_jars_old.len(), 1); +// +// assert_eq!(staked.0, 100_000); +// +// assert_eq!(ft_contract.ft_balance_of(bob.to_near()).await?.0, 900_000); +// +// drop(old_jar_contract); +// +// let new_jar_contract = jar_account.deploy(&jar_new_code).await?.into_result()?; +// let new_jar_contract = SweatJarContract { +// contract: &new_jar_contract, +// }; +// +// new_jar_contract.migrate_state_to_step_jars().await?; +// +// let products_new = new_jar_contract.get_products().with_user(&ft_account).await?; +// assert_eq!(products_old, products_new); +// +// let bob_jars_new = new_jar_contract.get_jars_for_account(bob.to_near()).await?; +// assert_eq!(bob_jars_old, bob_jars_new); +// +// new_jar_contract +// .register_product(RegisterProductCommand::Locked6Months6Percents.get()) +// .with_user(&manager_account) +// .await?; +// +// let products = new_jar_contract.get_products().with_user(&ft_account).await?; +// assert!(products.iter().all(|a| a.score_cap == 0)); +// assert_eq!(products.len(), 10); +// +// let staked = new_jar_contract +// .create_jar( +// &bob, +// RegisterProductCommand::Locked10Minutes6PercentsTopUp.get().id, +// 100_000, +// &ft_contract, +// ) +// .await?; +// +// let bob_jars = new_jar_contract.get_jars_for_account(bob.to_near()).await?; +// +// assert_eq!(bob_jars.len(), 2); +// +// assert_eq!(staked.0, 100_000); +// +// assert_eq!(ft_contract.ft_balance_of(bob.to_near()).await?.0, 800_000); +// +// new_jar_contract +// .register_product(RegisterProductCommand::Locked10Minutes20000ScoreCap.get()) +// .with_user(&manager_account) +// .await?; +// +// let products = new_jar_contract.get_products().with_user(&ft_account).await?; +// assert_eq!(products.last().unwrap().score_cap, 20_000); +// +// Ok(()) +// } diff --git a/integration-tests/src/premium_product.rs b/integration-tests/src/premium_product.rs index 488b09fb..aaae152e 100644 --- a/integration-tests/src/premium_product.rs +++ b/integration-tests/src/premium_product.rs @@ -1,96 +1,96 @@ -use base64::{engine::general_purpose::STANDARD, Engine}; -use ed25519_dalek::Signer; -use nitka::{misc::ToNear, near_sdk::serde_json::from_value}; -use sha2::{Digest, Sha256}; -use sweat_jar_model::api::{JarApiIntegration, PenaltyApiIntegration, ProductApiIntegration}; - -use crate::{ - common::generate_keypair, - context::{prepare_contract, IntegrationContext}, - jar_contract_extensions::JarContractExtensions, - product::RegisterProductCommand, -}; - -#[tokio::test] -#[mutants::skip] -async fn premium_product() -> anyhow::Result<()> { - println!("👷🏽 Run test for premium product"); - - let (signing_key, verifying_key) = generate_keypair(); - let pk_base64 = STANDARD.encode(verifying_key.as_bytes()); - - let mut context = prepare_contract(None, []).await?; - - let manager = context.manager().await?; - let alice = context.alice().await?; - - let register_product_command = RegisterProductCommand::Flexible6Months6Percents; - let command_json = register_product_command.json_for_premium(pk_base64); - - context - .sweat_jar() - .register_product(from_value(command_json).unwrap()) - .with_user(&manager) - .await?; - - let product_id = register_product_command.id(); - let valid_until = 43_012_170_000_000; - let amount = 3_000_000; - - let hash = Sha256::digest( - context - .sweat_jar() - .get_signature_material(&alice, &product_id, valid_until, amount, None) - .as_bytes(), - ); - - let signature = STANDARD.encode(signing_key.sign(hash.as_slice()).to_bytes()); - - let result = context - .sweat_jar() - .create_premium_jar( - &alice, - product_id.clone(), - amount, - signature.to_string(), - valid_until, - &context.ft_contract(), - ) - .await?; - - assert_eq!(result.0, amount); - - let jars = context.sweat_jar().get_jars_for_account(alice.to_near()).await?; - let jar_id = jars.first().unwrap().id; - - let jar = context.sweat_jar().get_jar(alice.to_near(), jar_id.clone()).await?; - - assert_eq!(jar.principal.0, amount); - assert!(!jar.is_penalty_applied); - - context - .sweat_jar() - .set_penalty(alice.to_near(), jar_id, true) - .with_user(&manager) - .await?; - - let jar = context.sweat_jar().get_jar(alice.to_near(), jar_id).await?; - - assert!(jar.is_penalty_applied); - - let unauthorized_penalty_change = context - .sweat_jar() - .set_penalty(alice.to_near(), jar_id, true) - .with_user(&alice) - .await; - - assert!(unauthorized_penalty_change.is_err()); - - let principal_result = context.sweat_jar().get_principal(vec![jar_id], alice.to_near()).await?; - assert_eq!(principal_result.total.0, amount); - - let interest_result = context.sweat_jar().get_interest(vec![jar_id], alice.to_near()).await; - assert!(interest_result.is_ok()); - - Ok(()) -} +// use base64::{engine::general_purpose::STANDARD, Engine}; +// use ed25519_dalek::Signer; +// use nitka::{misc::ToNear, near_sdk::serde_json::from_value}; +// use sha2::{Digest, Sha256}; +// use sweat_jar_model::api::{JarApiIntegration, PenaltyApiIntegration, ProductApiIntegration}; +// +// use crate::{ +// common::generate_keypair, +// context::{prepare_contract, IntegrationContext}, +// jar_contract_extensions::JarContractExtensions, +// product::RegisterProductCommand, +// }; +// +// #[tokio::test] +// #[mutants::skip] +// async fn premium_product() -> anyhow::Result<()> { +// println!("👷🏽 Run test for premium product"); +// +// let (signing_key, verifying_key) = generate_keypair(); +// let pk_base64 = STANDARD.encode(verifying_key.as_bytes()); +// +// let mut context = prepare_contract(None, []).await?; +// +// let manager = context.manager().await?; +// let alice = context.alice().await?; +// +// let register_product_command = RegisterProductCommand::Flexible6Months6Percents; +// let command_json = register_product_command.json_for_premium(pk_base64); +// +// context +// .sweat_jar() +// .register_product(from_value(command_json).unwrap()) +// .with_user(&manager) +// .await?; +// +// let product_id = register_product_command.id(); +// let valid_until = 43_012_170_000_000; +// let amount = 3_000_000; +// +// let hash = Sha256::digest( +// context +// .sweat_jar() +// .get_signature_material(&alice, &product_id, valid_until, amount, None) +// .as_bytes(), +// ); +// +// let signature = STANDARD.encode(signing_key.sign(hash.as_slice()).to_bytes()); +// +// let result = context +// .sweat_jar() +// .create_premium_jar( +// &alice, +// product_id.clone(), +// amount, +// signature.to_string(), +// valid_until, +// &context.ft_contract(), +// ) +// .await?; +// +// assert_eq!(result.0, amount); +// +// let jars = context.sweat_jar().get_jars_for_account(alice.to_near()).await?; +// let jar_id = jars.first().unwrap().id; +// +// let jar = context.sweat_jar().get_jar(alice.to_near(), jar_id.clone()).await?; +// +// assert_eq!(jar.principal.0, amount); +// assert!(!jar.is_penalty_applied); +// +// context +// .sweat_jar() +// .set_penalty(alice.to_near(), jar_id, true) +// .with_user(&manager) +// .await?; +// +// let jar = context.sweat_jar().get_jar(alice.to_near(), jar_id).await?; +// +// assert!(jar.is_penalty_applied); +// +// let unauthorized_penalty_change = context +// .sweat_jar() +// .set_penalty(alice.to_near(), jar_id, true) +// .with_user(&alice) +// .await; +// +// assert!(unauthorized_penalty_change.is_err()); +// +// let principal_result = context.sweat_jar().get_principal(vec![jar_id], alice.to_near()).await?; +// assert_eq!(principal_result.total.0, amount); +// +// let interest_result = context.sweat_jar().get_interest(vec![jar_id], alice.to_near()).await; +// assert!(interest_result.is_ok()); +// +// Ok(()) +// } diff --git a/integration-tests/src/restake.rs b/integration-tests/src/restake.rs index 8afd9a01..6d0ca6b3 100644 --- a/integration-tests/src/restake.rs +++ b/integration-tests/src/restake.rs @@ -1,138 +1,138 @@ -use anyhow::Result; -use nitka::{misc::ToNear, set_integration_logs_enabled}; -use sweat_jar_model::api::{ClaimApiIntegration, JarApiIntegration}; - -use crate::{ - context::{prepare_contract, ContextHelpers, IntegrationContext}, - jar_contract_extensions::JarContractExtensions, - product::RegisterProductCommand, -}; - -#[tokio::test] -#[mutants::skip] -async fn restake() -> Result<()> { - println!("👷🏽 Run test for restaking"); - - let product = RegisterProductCommand::Locked10Minutes6Percents; - - let mut context = prepare_contract(None, [product]).await?; - - let alice = context.alice().await?; - - let amount = 1_000_000; - context - .sweat_jar() - .create_jar(&alice, product.id(), amount, &context.ft_contract()) - .await?; - - let jars = context.sweat_jar().get_jars_for_account(alice.to_near()).await?; - let original_jar_id = jars.first().unwrap().id; - - context.fast_forward_hours(1).await?; - - context.sweat_jar().restake(original_jar_id).with_user(&alice).await?; - - let jars = context.sweat_jar().get_jars_for_account(alice.to_near()).await?; - assert_eq!(jars.len(), 2); - - let mut has_original_jar = false; - let mut has_restaked_jar = false; - for jar in jars { - let id = jar.id; - - if id == original_jar_id { - has_original_jar = true; - assert_eq!(jar.principal.0, 0); - } else { - has_restaked_jar = true; - assert_eq!(jar.principal.0, amount); - } - } - - assert!(has_original_jar); - assert!(has_restaked_jar); - - context.sweat_jar().claim_total(None).with_user(&alice).await?; - - let jars = context.sweat_jar().get_jars_for_account(alice.to_near()).await?; - assert_eq!(jars.len(), 1); - - Ok(()) -} - -#[tokio::test] -#[mutants::skip] -async fn restake_all() -> Result<()> { - const PRINCIPAL: u128 = 1_000_000; - const JARS_COUNT: u16 = 210; - - println!("👷🏽 Run test for restake all"); - - set_integration_logs_enabled(false); - - let product_5_min = RegisterProductCommand::Locked5Minutes60000Percents; - let product_10_min = RegisterProductCommand::Locked10Minutes60000Percents; - - let mut context = prepare_contract(None, [product_5_min, product_10_min]).await?; - - let alice = context.alice().await?; - - let amount = context - .sweat_jar() - .create_jar(&alice, product_5_min.id(), PRINCIPAL + 1, &context.ft_contract()) - .await?; - assert_eq!(amount.0, PRINCIPAL + 1); - - let jar_5_min_1 = context.last_jar_for(&alice).await?; - assert_eq!(jar_5_min_1.principal.0, PRINCIPAL + 1); - - context - .sweat_jar() - .create_jar(&alice, product_5_min.id(), PRINCIPAL + 2, &context.ft_contract()) - .await?; - let jar_5_min_2 = context.last_jar_for(&alice).await?; - assert_eq!(jar_5_min_2.principal.0, PRINCIPAL + 2); - - context - .sweat_jar() - .create_jar(&alice, product_10_min.id(), PRINCIPAL + 3, &context.ft_contract()) - .await?; - let jar_10_min = context.last_jar_for(&alice).await?; - assert_eq!(jar_10_min.principal.0, PRINCIPAL + 3); - - context - .bulk_create_jars(&alice, &product_5_min.id(), PRINCIPAL, JARS_COUNT) - .await?; - - let claimed = context.sweat_jar().claim_total(None).await?; - assert_eq!(claimed.get_total().0, 0); - - context.fast_forward_minutes(6).await?; - - context.sweat_jar().claim_total(None).with_user(&alice).await?; - - // Restaking in batches - let restaked = context.sweat_jar().restake_all(None).with_user(&alice).await?; - assert_eq!(restaked.len(), 200); - - let restaked_2 = context.sweat_jar().restake_all(None).with_user(&alice).await?; - assert_eq!(restaked_2.len(), 12); - - assert_eq!( - restaked.into_iter().map(|j| j.principal).collect::>()[..2], - vec![jar_5_min_1.principal, jar_5_min_2.principal] - ); - - let jars = context.sweat_jar().get_jars_for_account(alice.to_near()).await?; - - let principals = jars.iter().map(|j| j.principal.0).collect::>(); - - assert!( - [PRINCIPAL + 3, PRINCIPAL + 1, PRINCIPAL + 2] - .iter() - .all(|p| principals.contains(p)), - "Can't find all expected principals in {principals:?}" - ); - - Ok(()) -} +// use anyhow::Result; +// use nitka::{misc::ToNear, set_integration_logs_enabled}; +// use sweat_jar_model::api::{ClaimApiIntegration, JarApiIntegration}; +// +// use crate::{ +// context::{prepare_contract, ContextHelpers, IntegrationContext}, +// jar_contract_extensions::JarContractExtensions, +// product::RegisterProductCommand, +// }; +// +// #[tokio::test] +// #[mutants::skip] +// async fn restake() -> Result<()> { +// println!("👷🏽 Run test for restaking"); +// +// let product = RegisterProductCommand::Locked10Minutes6Percents; +// +// let mut context = prepare_contract(None, [product]).await?; +// +// let alice = context.alice().await?; +// +// let amount = 1_000_000; +// context +// .sweat_jar() +// .create_jar(&alice, product.id(), amount, &context.ft_contract()) +// .await?; +// +// let jars = context.sweat_jar().get_jars_for_account(alice.to_near()).await?; +// let original_jar_id = jars.first().unwrap().id; +// +// context.fast_forward_hours(1).await?; +// +// context.sweat_jar().restake(original_jar_id).with_user(&alice).await?; +// +// let jars = context.sweat_jar().get_jars_for_account(alice.to_near()).await?; +// assert_eq!(jars.len(), 2); +// +// let mut has_original_jar = false; +// let mut has_restaked_jar = false; +// for jar in jars { +// let id = jar.id; +// +// if id == original_jar_id { +// has_original_jar = true; +// assert_eq!(jar.principal.0, 0); +// } else { +// has_restaked_jar = true; +// assert_eq!(jar.principal.0, amount); +// } +// } +// +// assert!(has_original_jar); +// assert!(has_restaked_jar); +// +// context.sweat_jar().claim_total(None).with_user(&alice).await?; +// +// let jars = context.sweat_jar().get_jars_for_account(alice.to_near()).await?; +// assert_eq!(jars.len(), 1); +// +// Ok(()) +// } +// +// #[tokio::test] +// #[mutants::skip] +// async fn restake_all() -> Result<()> { +// const PRINCIPAL: u128 = 1_000_000; +// const JARS_COUNT: u16 = 210; +// +// println!("👷🏽 Run test for restake all"); +// +// set_integration_logs_enabled(false); +// +// let product_5_min = RegisterProductCommand::Locked5Minutes60000Percents; +// let product_10_min = RegisterProductCommand::Locked10Minutes60000Percents; +// +// let mut context = prepare_contract(None, [product_5_min, product_10_min]).await?; +// +// let alice = context.alice().await?; +// +// let amount = context +// .sweat_jar() +// .create_jar(&alice, product_5_min.id(), PRINCIPAL + 1, &context.ft_contract()) +// .await?; +// assert_eq!(amount.0, PRINCIPAL + 1); +// +// let jar_5_min_1 = context.last_jar_for(&alice).await?; +// assert_eq!(jar_5_min_1.principal.0, PRINCIPAL + 1); +// +// context +// .sweat_jar() +// .create_jar(&alice, product_5_min.id(), PRINCIPAL + 2, &context.ft_contract()) +// .await?; +// let jar_5_min_2 = context.last_jar_for(&alice).await?; +// assert_eq!(jar_5_min_2.principal.0, PRINCIPAL + 2); +// +// context +// .sweat_jar() +// .create_jar(&alice, product_10_min.id(), PRINCIPAL + 3, &context.ft_contract()) +// .await?; +// let jar_10_min = context.last_jar_for(&alice).await?; +// assert_eq!(jar_10_min.principal.0, PRINCIPAL + 3); +// +// context +// .bulk_create_jars(&alice, &product_5_min.id(), PRINCIPAL, JARS_COUNT) +// .await?; +// +// let claimed = context.sweat_jar().claim_total(None).await?; +// assert_eq!(claimed.get_total().0, 0); +// +// context.fast_forward_minutes(6).await?; +// +// context.sweat_jar().claim_total(None).with_user(&alice).await?; +// +// // Restaking in batches +// let restaked = context.sweat_jar().restake_all(None).with_user(&alice).await?; +// assert_eq!(restaked.len(), 200); +// +// let restaked_2 = context.sweat_jar().restake_all(None).with_user(&alice).await?; +// assert_eq!(restaked_2.len(), 12); +// +// assert_eq!( +// restaked.into_iter().map(|j| j.principal).collect::>()[..2], +// vec![jar_5_min_1.principal, jar_5_min_2.principal] +// ); +// +// let jars = context.sweat_jar().get_jars_for_account(alice.to_near()).await?; +// +// let principals = jars.iter().map(|j| j.principal.0).collect::>(); +// +// assert!( +// [PRINCIPAL + 3, PRINCIPAL + 1, PRINCIPAL + 2] +// .iter() +// .all(|p| principals.contains(p)), +// "Can't find all expected principals in {principals:?}" +// ); +// +// Ok(()) +// } diff --git a/integration-tests/src/testnet/recovery.rs b/integration-tests/src/testnet/recovery.rs index e3feee29..f01f797a 100644 --- a/integration-tests/src/testnet/recovery.rs +++ b/integration-tests/src/testnet/recovery.rs @@ -1,206 +1,206 @@ -use std::{fs::read_to_string, time::Duration}; - -use anyhow::Result; -use near_workspaces::Account; -use nitka::{ - misc::ToNear, - near_sdk::{serde_json, serde_json::Value}, -}; -use sweat_jar_model::{ - api::{ClaimApiIntegration, JarApiIntegration, ProductApiIntegration, SweatJarContract, WithdrawApiIntegration}, - claimed_amount_view::ClaimedAmountView, - product::{FixedProductTermsDto, ProductDto, TermsDto, WithdrawalFeeDto}, - MS_IN_DAY, MS_IN_SECOND, -}; -use tokio::time::sleep; - -use crate::{jar_contract_extensions::JarContractExtensions, testnet::testnet_context::TestnetContext}; - -fn _get_products() -> Vec { - let json_str = read_to_string("../products_testnet.json").unwrap(); - - let json: Value = serde_json::from_str(&json_str).unwrap(); - - let mut products: Vec = vec![]; - - for product_val in json.as_array().unwrap() { - let id = product_val["product_id"].as_str().unwrap().to_string(); - - let cap_min: u128 = product_val["min_amount"].as_str().unwrap().parse().unwrap(); - let cap_max: u128 = product_val["max_amount"].as_str().unwrap().parse().unwrap(); - - let pk = product_val["public_key"].as_str().unwrap(); - - #[allow(deprecated)] - let pk = base64::decode(pk).unwrap(); - - let is_enabled = product_val["is_enabled"].as_bool().unwrap(); - - let withdrawal_fee = { - let fixed: u128 = product_val["fee_fixed"].as_str().unwrap().parse().unwrap(); - let percent = product_val["fee_percent"].as_f64().unwrap(); - - if fixed != 0 { - Some(WithdrawalFeeDto::Fix(fixed.into())) - } else if percent != 0.0 { - Some(WithdrawalFeeDto::Percent(((percent * 1000.0) as u128).into(), 3)) - } else { - None - } - }; - - let apy = product_val["apy"].as_f64().unwrap(); - - let lockup_seconds = product_val["lockup_seconds"].as_u64().unwrap(); - - products.push(ProductDto { - id, - apy_default: (((apy * 1000.0) as u128).into(), 3), - apy_fallback: None, - cap_min: cap_min.into(), - cap_max: cap_max.into(), - terms: TermsDto::Fixed(FixedProductTermsDto { - lockup_term: (lockup_seconds * MS_IN_SECOND).into(), - allows_top_up: product_val["allows_top_up"].as_bool().unwrap(), - allows_restaking: product_val["allows_restaking"].as_bool().unwrap(), - }), - withdrawal_fee, - public_key: Some(pk.into()), - is_enabled, - score_cap: 0, - }) - } - - products -} - -async fn register_test_product(manager: &Account, jar: &SweatJarContract<'_>) -> Result<()> { - jar.register_product(ProductDto { - id: "5_days_20000_steps".to_string(), - apy_default: (0.into(), 0), - apy_fallback: None, - cap_min: 1_000_000.into(), - cap_max: 500000000000000000000000.into(), - terms: TermsDto::Fixed(FixedProductTermsDto { - lockup_term: (MS_IN_DAY * 5).into(), - allows_top_up: false, - allows_restaking: false, - }), - withdrawal_fee: None, - public_key: None, - is_enabled: true, - score_cap: 20_000, - }) - .with_user(manager) - .await?; - Ok(()) -} - -#[ignore] -#[tokio::test] -async fn register_product() -> Result<()> { - let ctx = TestnetContext::new().await?; - - register_test_product(&ctx.manager, &ctx.jar_contract()).await?; - - Ok(()) -} - -#[ignore] -#[tokio::test] -async fn create_many_jars() -> Result<()> { - let ctx = TestnetContext::new().await?; - - let jars = ctx.jar_contract().get_jars_for_account(ctx.user.to_near()).await?; - - dbg!(&jars.len()); - - for _ in 0..1000 { - ctx.jar_contract() - .create_jar( - &ctx.user, - "5min_50apy_restakable_no_signature".to_string(), - 1000000000000000000, - &ctx.token_contract(), - ) - .await? - .0; - } - - let jars = ctx.jar_contract().get_jars_for_account(ctx.user.to_near()).await?; - - dbg!(&jars.len()); - - Ok(()) -} - -#[ignore] -#[tokio::test] -/// Run this after testnet migration to check that everything runs as expected -async fn testnet_sanity_check() -> Result<()> { - const PRODUCT_ID: &str = "testnet_migration_test_product"; - const PRINCIPAL: u128 = 1222333334567778000; - - let ctx = TestnetContext::new().await?; - - let jars = ctx.jar_contract().get_jars_for_account(ctx.user.to_near()).await?; - - ctx.jar_contract() - .create_jar(&ctx.user, PRODUCT_ID.to_string(), PRINCIPAL, &ctx.token_contract()) - .await? - .0; - - let jars_after = ctx.jar_contract().get_jars_for_account(ctx.user.to_near()).await?; - - assert_eq!(jars.len() + 1, jars_after.len()); - - let new_jar = jars_after - .into_iter() - .filter(|item| !jars.contains(&item)) - .next() - .unwrap(); - - assert_eq!(new_jar.product_id, "testnet_migration_test_product"); - assert_eq!(new_jar.principal, PRINCIPAL.into()); - - sleep(Duration::from_secs(5)).await; - - let withdrawn = ctx.jar_contract().withdraw_all(None).with_user(&ctx.user).await?; - - assert!(withdrawn.jars.into_iter().any(|j| j.withdrawn_amount.0 == PRINCIPAL)); - - let ClaimedAmountView::Detailed(claimed) = ctx.jar_contract().claim_total(Some(true)).with_user(&ctx.user).await? - else { - panic!() - }; - - let claimed_jar = claimed.detailed.get(&new_jar.id).expect("New jar not found"); - - assert_eq!(claimed_jar.0, 193799678869); - - let jars = ctx.jar_contract().get_jars_for_account(ctx.user.to_near()).await?; - - // Jar is deleted after full claim and withdraw - assert!(!jars.into_iter().any(|j| j.id == new_jar.id)); - - Ok(()) -} - -#[ignore] -#[tokio::test] -async fn sandbox() -> Result<()> { - let ctx = TestnetContext::new().await?; - - let jars = ctx.jar_contract().get_jars_for_account(ctx.user2.to_near()).await?; - dbg!(&jars); - - ctx.jar_contract() - .unlock_jars_for_account(ctx.user2.to_near()) - .with_user(&ctx.manager) - .await?; - - let jars = ctx.jar_contract().get_jars_for_account(ctx.user2.to_near()).await?; - dbg!(&jars); - - Ok(()) -} +// use std::{fs::read_to_string, time::Duration}; +// +// use anyhow::Result; +// use near_workspaces::Account; +// use nitka::{ +// misc::ToNear, +// near_sdk::{serde_json, serde_json::Value}, +// }; +// use sweat_jar_model::{ +// api::{ClaimApiIntegration, JarApiIntegration, ProductApiIntegration, SweatJarContract, WithdrawApiIntegration}, +// claimed_amount_view::ClaimedAmountView, +// product::{FixedProductTermsDto, ProductDto, TermsDto, WithdrawalFeeDto}, +// MS_IN_DAY, MS_IN_SECOND, +// }; +// use tokio::time::sleep; +// +// use crate::{jar_contract_extensions::JarContractExtensions, testnet::testnet_context::TestnetContext}; +// +// fn _get_products() -> Vec { +// let json_str = read_to_string("../products_testnet.json").unwrap(); +// +// let json: Value = serde_json::from_str(&json_str).unwrap(); +// +// let mut products: Vec = vec![]; +// +// for product_val in json.as_array().unwrap() { +// let id = product_val["product_id"].as_str().unwrap().to_string(); +// +// let cap_min: u128 = product_val["min_amount"].as_str().unwrap().parse().unwrap(); +// let cap_max: u128 = product_val["max_amount"].as_str().unwrap().parse().unwrap(); +// +// let pk = product_val["public_key"].as_str().unwrap(); +// +// #[allow(deprecated)] +// let pk = base64::decode(pk).unwrap(); +// +// let is_enabled = product_val["is_enabled"].as_bool().unwrap(); +// +// let withdrawal_fee = { +// let fixed: u128 = product_val["fee_fixed"].as_str().unwrap().parse().unwrap(); +// let percent = product_val["fee_percent"].as_f64().unwrap(); +// +// if fixed != 0 { +// Some(WithdrawalFeeDto::Fix(fixed.into())) +// } else if percent != 0.0 { +// Some(WithdrawalFeeDto::Percent(((percent * 1000.0) as u128).into(), 3)) +// } else { +// None +// } +// }; +// +// let apy = product_val["apy"].as_f64().unwrap(); +// +// let lockup_seconds = product_val["lockup_seconds"].as_u64().unwrap(); +// +// products.push(ProductDto { +// id, +// apy_default: (((apy * 1000.0) as u128).into(), 3), +// apy_fallback: None, +// cap_min: cap_min.into(), +// cap_max: cap_max.into(), +// terms: TermsDto::Fixed(FixedProductTermsDto { +// lockup_term: (lockup_seconds * MS_IN_SECOND).into(), +// allows_top_up: product_val["allows_top_up"].as_bool().unwrap(), +// allows_restaking: product_val["allows_restaking"].as_bool().unwrap(), +// }), +// withdrawal_fee, +// public_key: Some(pk.into()), +// is_enabled, +// score_cap: 0, +// }) +// } +// +// products +// } +// +// async fn register_test_product(manager: &Account, jar: &SweatJarContract<'_>) -> Result<()> { +// jar.register_product(ProductDto { +// id: "5_days_20000_steps".to_string(), +// apy_default: (0.into(), 0), +// apy_fallback: None, +// cap_min: 1_000_000.into(), +// cap_max: 500000000000000000000000.into(), +// terms: TermsDto::Fixed(FixedProductTermsDto { +// lockup_term: (MS_IN_DAY * 5).into(), +// allows_top_up: false, +// allows_restaking: false, +// }), +// withdrawal_fee: None, +// public_key: None, +// is_enabled: true, +// score_cap: 20_000, +// }) +// .with_user(manager) +// .await?; +// Ok(()) +// } +// +// #[ignore] +// #[tokio::test] +// async fn register_product() -> Result<()> { +// let ctx = TestnetContext::new().await?; +// +// register_test_product(&ctx.manager, &ctx.jar_contract()).await?; +// +// Ok(()) +// } +// +// #[ignore] +// #[tokio::test] +// async fn create_many_jars() -> Result<()> { +// let ctx = TestnetContext::new().await?; +// +// let jars = ctx.jar_contract().get_jars_for_account(ctx.user.to_near()).await?; +// +// dbg!(&jars.len()); +// +// for _ in 0..1000 { +// ctx.jar_contract() +// .create_jar( +// &ctx.user, +// "5min_50apy_restakable_no_signature".to_string(), +// 1000000000000000000, +// &ctx.token_contract(), +// ) +// .await? +// .0; +// } +// +// let jars = ctx.jar_contract().get_jars_for_account(ctx.user.to_near()).await?; +// +// dbg!(&jars.len()); +// +// Ok(()) +// } +// +// #[ignore] +// #[tokio::test] +// /// Run this after testnet migration to check that everything runs as expected +// async fn testnet_sanity_check() -> Result<()> { +// const PRODUCT_ID: &str = "testnet_migration_test_product"; +// const PRINCIPAL: u128 = 1222333334567778000; +// +// let ctx = TestnetContext::new().await?; +// +// let jars = ctx.jar_contract().get_jars_for_account(ctx.user.to_near()).await?; +// +// ctx.jar_contract() +// .create_jar(&ctx.user, PRODUCT_ID.to_string(), PRINCIPAL, &ctx.token_contract()) +// .await? +// .0; +// +// let jars_after = ctx.jar_contract().get_jars_for_account(ctx.user.to_near()).await?; +// +// assert_eq!(jars.len() + 1, jars_after.len()); +// +// let new_jar = jars_after +// .into_iter() +// .filter(|item| !jars.contains(&item)) +// .next() +// .unwrap(); +// +// assert_eq!(new_jar.product_id, "testnet_migration_test_product"); +// assert_eq!(new_jar.principal, PRINCIPAL.into()); +// +// sleep(Duration::from_secs(5)).await; +// +// let withdrawn = ctx.jar_contract().withdraw_all(None).with_user(&ctx.user).await?; +// +// assert!(withdrawn.jars.into_iter().any(|j| j.withdrawn_amount.0 == PRINCIPAL)); +// +// let ClaimedAmountView::Detailed(claimed) = ctx.jar_contract().claim_total(Some(true)).with_user(&ctx.user).await? +// else { +// panic!() +// }; +// +// let claimed_jar = claimed.detailed.get(&new_jar.id).expect("New jar not found"); +// +// assert_eq!(claimed_jar.0, 193799678869); +// +// let jars = ctx.jar_contract().get_jars_for_account(ctx.user.to_near()).await?; +// +// // Jar is deleted after full claim and withdraw +// assert!(!jars.into_iter().any(|j| j.id == new_jar.id)); +// +// Ok(()) +// } +// +// #[ignore] +// #[tokio::test] +// async fn sandbox() -> Result<()> { +// let ctx = TestnetContext::new().await?; +// +// let jars = ctx.jar_contract().get_jars_for_account(ctx.user2.to_near()).await?; +// dbg!(&jars); +// +// ctx.jar_contract() +// .unlock_jars_for_account(ctx.user2.to_near()) +// .with_user(&ctx.manager) +// .await?; +// +// let jars = ctx.jar_contract().get_jars_for_account(ctx.user2.to_near()).await?; +// dbg!(&jars); +// +// Ok(()) +// } diff --git a/integration-tests/src/withdraw_all.rs b/integration-tests/src/withdraw_all.rs index 9ae6a702..d930f879 100644 --- a/integration-tests/src/withdraw_all.rs +++ b/integration-tests/src/withdraw_all.rs @@ -1,105 +1,105 @@ -use anyhow::Result; -use nitka::{misc::ToNear, set_integration_logs_enabled}; -use sweat_jar_model::api::{ClaimApiIntegration, JarApiIntegration, WithdrawApiIntegration}; -use sweat_model::FungibleTokenCoreIntegration; - -use crate::{ - context::{prepare_contract, ContextHelpers, IntegrationContext}, - jar_contract_extensions::JarContractExtensions, - product::RegisterProductCommand, -}; - -#[tokio::test] -#[mutants::skip] -async fn withdraw_all() -> Result<()> { - const PRINCIPAL: u128 = 1_000_000; - const JARS_COUNT: u16 = 210; - const BULK_PRINCIPAL: u128 = PRINCIPAL * JARS_COUNT as u128; - - println!("👷🏽 Run test for withdraw all"); - - set_integration_logs_enabled(false); - - let product_5_min = RegisterProductCommand::Locked5Minutes60000Percents; - let product_10_min = RegisterProductCommand::Locked10Minutes60000Percents; - - let mut context = prepare_contract(None, [product_5_min, product_10_min]).await?; - - let alice = context.alice().await?; - - let amount = context - .sweat_jar() - .create_jar(&alice, product_5_min.id(), PRINCIPAL + 1, &context.ft_contract()) - .await?; - assert_eq!(amount.0, PRINCIPAL + 1); - - let jar_5_min_1 = context.last_jar_for(&alice).await?; - assert_eq!(jar_5_min_1.principal.0, PRINCIPAL + 1); - - context - .sweat_jar() - .create_jar(&alice, product_5_min.id(), PRINCIPAL + 2, &context.ft_contract()) - .await?; - let jar_5_min_2 = context.last_jar_for(&alice).await?; - assert_eq!(jar_5_min_2.principal.0, PRINCIPAL + 2); - - context - .bulk_create_jars(&alice, &product_5_min.id(), PRINCIPAL, JARS_COUNT) - .await?; - - context - .sweat_jar() - .create_jar(&alice, product_10_min.id(), PRINCIPAL + 3, &context.ft_contract()) - .await?; - let jar_10_min = context.last_jar_for(&alice).await?; - assert_eq!(jar_10_min.principal.0, PRINCIPAL + 3); - - let claimed = context.sweat_jar().claim_total(None).await?; - assert_eq!(claimed.get_total().0, 0); - - context.fast_forward_minutes(6).await?; - - // 2 calls to claim all 210 jars - context.sweat_jar().claim_total(None).with_user(&alice).await?; - context.sweat_jar().claim_total(None).with_user(&alice).await?; - - let alice_balance = context.ft_contract().ft_balance_of(alice.to_near()).await?; - let jar_balance = context - .ft_contract() - .ft_balance_of(context.sweat_jar().contract.as_account().to_near()) - .await?; - - let withdrawn = context.sweat_jar().withdraw_all(None).with_user(&alice).await?; - assert_eq!(withdrawn.jars.len(), 200); - - let withdrawn_2 = context.sweat_jar().withdraw_all(None).with_user(&alice).await?; - assert_eq!(withdrawn_2.jars.len(), 12); - - let alice_balance_after = context.ft_contract().ft_balance_of(alice.to_near()).await?; - let jar_balance_after = context - .ft_contract() - .ft_balance_of(context.sweat_jar().contract.as_account().to_near()) - .await?; - - assert_eq!(alice_balance_after.0 - alice_balance.0, BULK_PRINCIPAL + 2000003); - assert_eq!(jar_balance.0 - jar_balance_after.0, BULK_PRINCIPAL + 2000003); - - assert_eq!(withdrawn.total_amount.0, 200000003); - assert_eq!(withdrawn_2.total_amount.0, PRINCIPAL * 12); - - assert_eq!( - withdrawn.jars.iter().map(|j| j.withdrawn_amount).collect::>()[..2], - vec![jar_5_min_1.principal, jar_5_min_2.principal] - ); - - let jars = context.sweat_jar().get_jars_for_account(alice.to_near()).await?; - - assert_eq!(jars.len(), 1); - - let jar = jars.into_iter().next().unwrap(); - - assert_eq!(jar.id, jar_10_min.id); - assert_eq!(jar.principal, jar_10_min.principal); - - Ok(()) -} +// use anyhow::Result; +// use nitka::{misc::ToNear, set_integration_logs_enabled}; +// use sweat_jar_model::api::{ClaimApiIntegration, JarApiIntegration, WithdrawApiIntegration}; +// use sweat_model::FungibleTokenCoreIntegration; +// +// use crate::{ +// context::{prepare_contract, ContextHelpers, IntegrationContext}, +// jar_contract_extensions::JarContractExtensions, +// product::RegisterProductCommand, +// }; +// +// #[tokio::test] +// #[mutants::skip] +// async fn withdraw_all() -> Result<()> { +// const PRINCIPAL: u128 = 1_000_000; +// const JARS_COUNT: u16 = 210; +// const BULK_PRINCIPAL: u128 = PRINCIPAL * JARS_COUNT as u128; +// +// println!("👷🏽 Run test for withdraw all"); +// +// set_integration_logs_enabled(false); +// +// let product_5_min = RegisterProductCommand::Locked5Minutes60000Percents; +// let product_10_min = RegisterProductCommand::Locked10Minutes60000Percents; +// +// let mut context = prepare_contract(None, [product_5_min, product_10_min]).await?; +// +// let alice = context.alice().await?; +// +// let amount = context +// .sweat_jar() +// .create_jar(&alice, product_5_min.id(), PRINCIPAL + 1, &context.ft_contract()) +// .await?; +// assert_eq!(amount.0, PRINCIPAL + 1); +// +// let jar_5_min_1 = context.last_jar_for(&alice).await?; +// assert_eq!(jar_5_min_1.principal.0, PRINCIPAL + 1); +// +// context +// .sweat_jar() +// .create_jar(&alice, product_5_min.id(), PRINCIPAL + 2, &context.ft_contract()) +// .await?; +// let jar_5_min_2 = context.last_jar_for(&alice).await?; +// assert_eq!(jar_5_min_2.principal.0, PRINCIPAL + 2); +// +// context +// .bulk_create_jars(&alice, &product_5_min.id(), PRINCIPAL, JARS_COUNT) +// .await?; +// +// context +// .sweat_jar() +// .create_jar(&alice, product_10_min.id(), PRINCIPAL + 3, &context.ft_contract()) +// .await?; +// let jar_10_min = context.last_jar_for(&alice).await?; +// assert_eq!(jar_10_min.principal.0, PRINCIPAL + 3); +// +// let claimed = context.sweat_jar().claim_total(None).await?; +// assert_eq!(claimed.get_total().0, 0); +// +// context.fast_forward_minutes(6).await?; +// +// // 2 calls to claim all 210 jars +// context.sweat_jar().claim_total(None).with_user(&alice).await?; +// context.sweat_jar().claim_total(None).with_user(&alice).await?; +// +// let alice_balance = context.ft_contract().ft_balance_of(alice.to_near()).await?; +// let jar_balance = context +// .ft_contract() +// .ft_balance_of(context.sweat_jar().contract.as_account().to_near()) +// .await?; +// +// let withdrawn = context.sweat_jar().withdraw_all(None).with_user(&alice).await?; +// assert_eq!(withdrawn.jars.len(), 200); +// +// let withdrawn_2 = context.sweat_jar().withdraw_all(None).with_user(&alice).await?; +// assert_eq!(withdrawn_2.jars.len(), 12); +// +// let alice_balance_after = context.ft_contract().ft_balance_of(alice.to_near()).await?; +// let jar_balance_after = context +// .ft_contract() +// .ft_balance_of(context.sweat_jar().contract.as_account().to_near()) +// .await?; +// +// assert_eq!(alice_balance_after.0 - alice_balance.0, BULK_PRINCIPAL + 2000003); +// assert_eq!(jar_balance.0 - jar_balance_after.0, BULK_PRINCIPAL + 2000003); +// +// assert_eq!(withdrawn.total_amount.0, 200000003); +// assert_eq!(withdrawn_2.total_amount.0, PRINCIPAL * 12); +// +// assert_eq!( +// withdrawn.jars.iter().map(|j| j.withdrawn_amount).collect::>()[..2], +// vec![jar_5_min_1.principal, jar_5_min_2.principal] +// ); +// +// let jars = context.sweat_jar().get_jars_for_account(alice.to_near()).await?; +// +// assert_eq!(jars.len(), 1); +// +// let jar = jars.into_iter().next().unwrap(); +// +// assert_eq!(jar.id, jar_10_min.id); +// assert_eq!(jar.principal, jar_10_min.principal); +// +// Ok(()) +// } diff --git a/integration-tests/src/withdraw_fee.rs b/integration-tests/src/withdraw_fee.rs index a1ddccbb..e3af7b28 100644 --- a/integration-tests/src/withdraw_fee.rs +++ b/integration-tests/src/withdraw_fee.rs @@ -1,102 +1,102 @@ -use nitka::misc::ToNear; -use sweat_jar_model::{api::WithdrawApiIntegration, U32}; -use sweat_model::FungibleTokenCoreIntegration; - -use crate::{ - context::{prepare_contract, IntegrationContext}, - jar_contract_extensions::JarContractExtensions, - product::RegisterProductCommand, -}; - -#[tokio::test] -#[mutants::skip] -async fn test_fixed_withdraw_fee() -> anyhow::Result<()> { - println!("👷🏽 Run fixed withdraw fee test"); - - let mut context = prepare_contract( - None, - [RegisterProductCommand::Locked10Minutes6PercentsWithFixedWithdrawFee], - ) - .await?; - - let alice = context.alice().await?; - let fee_account = context.fee().await?; - - let fee_balance_before = context.ft_contract().ft_balance_of(fee_account.to_near()).await?.0; - - context - .sweat_jar() - .create_jar( - &alice, - RegisterProductCommand::Locked10Minutes6PercentsWithFixedWithdrawFee.id(), - 1_000_000, - &context.ft_contract(), - ) - .await?; - - let mut alice_balance = context.ft_contract().ft_balance_of(alice.to_near()).await?; - assert_eq!(99_000_000, alice_balance.0); - - context.fast_forward_hours(1).await?; - - let withdraw_result = context.sweat_jar().withdraw(U32(1), None).with_user(&alice).await?; - let withdrawn_amount = withdraw_result.withdrawn_amount; - let fee_amount = withdraw_result.fee; - - assert_eq!(999_000, withdrawn_amount.0); - assert_eq!(1_000, fee_amount.0); - - alice_balance = context.ft_contract().ft_balance_of(alice.to_near()).await?; - assert_eq!(99_999_000, alice_balance.0); - - let fee_balance_after = context.ft_contract().ft_balance_of(fee_account.to_near()).await?.0; - assert_eq!(1_000, fee_balance_after - fee_balance_before); - - Ok(()) -} - -#[tokio::test] -async fn test_percent_withdraw_fee() -> anyhow::Result<()> { - println!("👷🏽 Run percent withdraw fee test"); - - let mut context = prepare_contract( - None, - [RegisterProductCommand::Locked10Minutes6PercentsWithPercentWithdrawFee], - ) - .await?; - - let alice = context.alice().await?; - let fee_account = context.fee().await?; - - let fee_balance_before = context.ft_contract().ft_balance_of(fee_account.to_near()).await?.0; - - context - .sweat_jar() - .create_jar( - &alice, - RegisterProductCommand::Locked10Minutes6PercentsWithPercentWithdrawFee.id(), - 1_000_000, - &context.ft_contract(), - ) - .await?; - - let mut alice_balance = context.ft_contract().ft_balance_of(alice.to_near()).await?; - assert_eq!(99_000_000, alice_balance.0); - - context.fast_forward_hours(1).await?; - - let withdraw_result = context.sweat_jar().withdraw(U32(1), None).with_user(&alice).await?; - let withdrawn_amount = withdraw_result.withdrawn_amount; - let fee_amount = withdraw_result.fee; - - assert_eq!(990_000, withdrawn_amount.0); - assert_eq!(10_000, fee_amount.0); - - alice_balance = context.ft_contract().ft_balance_of(alice.to_near()).await?; - assert_eq!(99_990_000, alice_balance.0); - - let fee_balance_after = context.ft_contract().ft_balance_of(fee_account.to_near()).await?.0; - assert_eq!(10_000, fee_balance_after - fee_balance_before); - - Ok(()) -} +// use nitka::misc::ToNear; +// use sweat_jar_model::{api::WithdrawApiIntegration, U32}; +// use sweat_model::FungibleTokenCoreIntegration; +// +// use crate::{ +// context::{prepare_contract, IntegrationContext}, +// jar_contract_extensions::JarContractExtensions, +// product::RegisterProductCommand, +// }; +// +// #[tokio::test] +// #[mutants::skip] +// async fn test_fixed_withdraw_fee() -> anyhow::Result<()> { +// println!("👷🏽 Run fixed withdraw fee test"); +// +// let mut context = prepare_contract( +// None, +// [RegisterProductCommand::Locked10Minutes6PercentsWithFixedWithdrawFee], +// ) +// .await?; +// +// let alice = context.alice().await?; +// let fee_account = context.fee().await?; +// +// let fee_balance_before = context.ft_contract().ft_balance_of(fee_account.to_near()).await?.0; +// +// context +// .sweat_jar() +// .create_jar( +// &alice, +// RegisterProductCommand::Locked10Minutes6PercentsWithFixedWithdrawFee.id(), +// 1_000_000, +// &context.ft_contract(), +// ) +// .await?; +// +// let mut alice_balance = context.ft_contract().ft_balance_of(alice.to_near()).await?; +// assert_eq!(99_000_000, alice_balance.0); +// +// context.fast_forward_hours(1).await?; +// +// let withdraw_result = context.sweat_jar().withdraw(U32(1), None).with_user(&alice).await?; +// let withdrawn_amount = withdraw_result.withdrawn_amount; +// let fee_amount = withdraw_result.fee; +// +// assert_eq!(999_000, withdrawn_amount.0); +// assert_eq!(1_000, fee_amount.0); +// +// alice_balance = context.ft_contract().ft_balance_of(alice.to_near()).await?; +// assert_eq!(99_999_000, alice_balance.0); +// +// let fee_balance_after = context.ft_contract().ft_balance_of(fee_account.to_near()).await?.0; +// assert_eq!(1_000, fee_balance_after - fee_balance_before); +// +// Ok(()) +// } +// +// #[tokio::test] +// async fn test_percent_withdraw_fee() -> anyhow::Result<()> { +// println!("👷🏽 Run percent withdraw fee test"); +// +// let mut context = prepare_contract( +// None, +// [RegisterProductCommand::Locked10Minutes6PercentsWithPercentWithdrawFee], +// ) +// .await?; +// +// let alice = context.alice().await?; +// let fee_account = context.fee().await?; +// +// let fee_balance_before = context.ft_contract().ft_balance_of(fee_account.to_near()).await?.0; +// +// context +// .sweat_jar() +// .create_jar( +// &alice, +// RegisterProductCommand::Locked10Minutes6PercentsWithPercentWithdrawFee.id(), +// 1_000_000, +// &context.ft_contract(), +// ) +// .await?; +// +// let mut alice_balance = context.ft_contract().ft_balance_of(alice.to_near()).await?; +// assert_eq!(99_000_000, alice_balance.0); +// +// context.fast_forward_hours(1).await?; +// +// let withdraw_result = context.sweat_jar().withdraw(U32(1), None).with_user(&alice).await?; +// let withdrawn_amount = withdraw_result.withdrawn_amount; +// let fee_amount = withdraw_result.fee; +// +// assert_eq!(990_000, withdrawn_amount.0); +// assert_eq!(10_000, fee_amount.0); +// +// alice_balance = context.ft_contract().ft_balance_of(alice.to_near()).await?; +// assert_eq!(99_990_000, alice_balance.0); +// +// let fee_balance_after = context.ft_contract().ft_balance_of(fee_account.to_near()).await?.0; +// assert_eq!(10_000, fee_balance_after - fee_balance_before); +// +// Ok(()) +// } From e186ac7c4d8e74be7d37c50c48020be591be5fa9 Mon Sep 17 00:00:00 2001 From: Vasily Styagov Date: Mon, 14 Oct 2024 19:20:13 +0100 Subject: [PATCH 13/93] fix account borrow checker errors --- contract/src/jar/account/v2.rs | 41 +++++++++++++++++++++------------- contract/src/jar/model/v2.rs | 6 ++--- 2 files changed, 28 insertions(+), 19 deletions(-) diff --git a/contract/src/jar/account/v2.rs b/contract/src/jar/account/v2.rs index 3651cf0a..54bf9f0b 100644 --- a/contract/src/jar/account/v2.rs +++ b/contract/src/jar/account/v2.rs @@ -87,13 +87,13 @@ impl AccountV2 { } pub(crate) fn try_set_timezone(&mut self, timezone: Option) { - match (timezone, self.score.borrow_mut()) { + match (timezone, self.score.is_valid()) { // Time zone already set. No actions required. - (Some(_) | None, Some(_)) => (), - (Some(timezone), None) => { + (Some(_) | None, true) => (), + (Some(timezone), false) => { self.score = AccountScore::new(timezone); } - (None, None) => { + (None, false) => { panic_str("Trying to create score based jar for without providing time zone"); } } @@ -104,9 +104,9 @@ impl AccountV2 { self.nonce = nonce; } - if let Some(jars) = companion.jars.iter() { + if let Some(jars) = &companion.jars { for (product_id, jar_companion) in jars { - let jar = self.jars.get_mut(&product_id).expect("Jar is not found"); + let jar = self.jars.get_mut(product_id).expect("Jar is not found"); jar.apply(jar_companion); } } @@ -119,30 +119,39 @@ impl AccountV2 { self.is_penalty_applied = is_penalty_applied; } } + + fn update_jar_cache(&mut self, product: &ProductV2, now: Timestamp) { + let jar = self.get_jar(&product.id); + let (interest, remainder) = product.terms.get_interest(self, jar); + self.get_jar_mut(&product.id).update_cache(interest, remainder, now); + } } impl Contract { pub(crate) fn update_account_cache(&mut self, account_id: &AccountId) { let now = env::block_timestamp_ms(); - let account = self.get_account_mut(account_id); - - for (product_id, jar) in account.jars.iter_mut() { - let product = &self.get_product(product_id); - jar.update_cache(account, product, now); + let products: Vec = self + .get_account(&account_id) + .jars + .iter() + .map(|(product_id, _)| self.get_product(product_id)) + .collect(); + + let account = self.get_account_mut(&account_id); + for product in products { + account.update_jar_cache(&product, now); } } pub(crate) fn update_jar_cache(&mut self, account_id: &AccountId, product_id: &ProductId) { - let account = self.get_account_mut(account_id); let product = &self.get_product(product_id); - let jar = account.get_jar_mut(product_id); - jar.update_cache(account, product, env::block_timestamp_ms()); + let account = self.get_account_mut(account_id); + account.update_jar_cache(product, env::block_timestamp_ms()); } } impl JarV2 { - fn update_cache(&mut self, account: &AccountV2, product: &ProductV2, now: Timestamp) { - let (interest, remainder) = product.terms.get_interest(account, self); + fn update_cache(&mut self, interest: TokenAmount, remainder: u64, now: Timestamp) { self.cache = Some(JarCache { updated_at: now, interest, diff --git a/contract/src/jar/model/v2.rs b/contract/src/jar/model/v2.rs index a79b6c52..4873c866 100644 --- a/contract/src/jar/model/v2.rs +++ b/contract/src/jar/model/v2.rs @@ -96,7 +96,7 @@ impl JarV2 { } } - pub(crate) fn apply(&mut self, companion: JarV2Companion) -> &mut Self { + pub(crate) fn apply(&mut self, companion: &JarV2Companion) -> &mut Self { if let Some(claim_remainder) = companion.claim_remainder { self.claim_remainder = claim_remainder; } @@ -109,8 +109,8 @@ impl JarV2 { self.cache = cache; } - if let Some(deposits) = companion.deposits { - self.deposits = deposits; + if let Some(deposits) = &companion.deposits { + self.deposits = deposits.iter().cloned().collect(); } if let Some(is_pending_withdraw) = companion.is_pending_withdraw { From 2e091068dea9b9808ef24b332b8a73f5650024c1 Mon Sep 17 00:00:00 2001 From: Vasily Styagov Date: Tue, 15 Oct 2024 17:00:40 +0100 Subject: [PATCH 14/93] fix build --- contract/src/assert.rs | 4 - contract/src/claim/api.rs | 20 +++- contract/src/ft_receiver.rs | 106 +++++++++--------- contract/src/internal.rs | 15 ++- contract/src/jar/account/v1.rs | 2 +- contract/src/jar/account/versioned.rs | 2 +- contract/src/jar/api.rs | 74 ++++++------ contract/src/jar/model/common.rs | 17 +-- contract/src/jar/model/v2.rs | 3 +- contract/src/jar/view.rs | 8 +- .../migration/account_jars_non_versioned.rs | 2 +- contract/src/migration/step_jars.rs | 94 ++++++++-------- contract/src/penalty/api.rs | 4 +- contract/src/product/{command.rs => dto.rs} | 0 contract/src/product/mod.rs | 2 +- contract/src/product/model/v2.rs | 15 ++- contract/src/score/score_api.rs | 49 ++------ model/src/api.rs | 2 +- model/src/udecimal.rs | 8 +- res/sweat_jar.wasm | Bin 619370 -> 635959 bytes 20 files changed, 206 insertions(+), 221 deletions(-) rename contract/src/product/{command.rs => dto.rs} (100%) diff --git a/contract/src/assert.rs b/contract/src/assert.rs index a9fafb61..2cbf936c 100644 --- a/contract/src/assert.rs +++ b/contract/src/assert.rs @@ -6,7 +6,3 @@ use crate::jar::model::{Jar, JarV2}; pub(crate) fn assert_not_locked(jar: &JarV2) { require!(!jar.is_pending_withdraw, "Another operation on this Jar is in progress"); } - -pub(crate) fn assert_sufficient_balance(jar: &Jar, amount: TokenAmount) { - require!(jar.principal >= amount, "Insufficient balance"); -} diff --git a/contract/src/claim/api.rs b/contract/src/claim/api.rs index 0dfb416d..62294630 100644 --- a/contract/src/claim/api.rs +++ b/contract/src/claim/api.rs @@ -1,7 +1,9 @@ use std::collections::HashMap; use near_sdk::{env, ext_contract, json_types::U128, near_bindgen, AccountId, PromiseOrValue}; -use sweat_jar_model::{api::ClaimApi, claimed_amount_view::ClaimedAmountView, jar::AggregatedTokenAmountView}; +use sweat_jar_model::{ + api::ClaimApi, claimed_amount_view::ClaimedAmountView, jar::AggregatedTokenAmountView, ProductId, TokenAmount, +}; use crate::{ event::{emit, EventKind}, @@ -46,12 +48,14 @@ impl Contract { let now = env::block_timestamp_ms(); let mut rollback_jars = HashMap::new(); - for (product_id, jar) in account.jars.iter_mut() { + let mut interest_per_jar: HashMap = HashMap::new(); + + for (product_id, jar) in account.jars.iter() { if jar.is_pending_withdraw { continue; } - rollback_jars.insert(product_id, jar.to_rollback()); + rollback_jars.insert(product_id.clone(), jar.to_rollback()); let product = self.products.get(product_id).expect("Product is not found"); let (interest, remainder) = product.terms.get_interest(account, jar); @@ -60,14 +64,18 @@ impl Contract { continue; } - jar.claim(interest, remainder, now).lock(); - + interest_per_jar.insert(product_id.clone(), (interest, remainder)); accumulator.add(product_id, interest); } + for (product_id, (interest, remainder)) in interest_per_jar { + let jar = account.get_jar_mut(&product_id); + jar.claim(interest, remainder, now).lock(); + } + let mut account_rollback = AccountV2Companion::default(); account_rollback.score = Some(account.score); - account_rollback.jars = Some(*rollback_jars); + account_rollback.jars = Some(rollback_jars); account.score.claim_score(); diff --git a/contract/src/ft_receiver.rs b/contract/src/ft_receiver.rs index e0078817..db9ac9a2 100644 --- a/contract/src/ft_receiver.rs +++ b/contract/src/ft_receiver.rs @@ -1,56 +1,56 @@ -// use near_contract_standards::fungible_token::receiver::FungibleTokenReceiver; -// use near_sdk::{json_types::U128, near, require, serde_json, AccountId, PromiseOrValue}; -// use sweat_jar_model::jar::CeFiJar; -// -// use crate::{jar::model::JarTicket, near_bindgen, Base64VecU8, Contract, ContractExt}; -// -// /// The `FtMessage` enum represents various commands for actions available via transferring tokens to an account -// /// where this contract is deployed, using the payload in `ft_transfer_call`. -// #[near(serializers=[json])] -// #[serde(tag = "type", content = "data", rename_all = "snake_case")] -// pub enum FtMessage { -// /// Represents a request to create a new jar for a corresponding product. -// Stake(StakeMessage), -// -// /// Represents a request to create `DeFi` Jars from provided `CeFi` Jars. -// Migrate(Vec), -// } -// -// /// The `StakeMessage` struct represents a request to create a new jar for a corresponding product. -// #[near(serializers=[json])] -// pub struct StakeMessage { -// /// Data of the `JarTicket` required for validating the request and specifying the product. -// ticket: JarTicket, -// -// /// An optional ed25519 signature used to verify the authenticity of the request. -// signature: Option, -// -// /// An optional account ID representing the intended owner of the created jar. -// receiver_id: Option, -// } -// -// #[near_bindgen] -// impl FungibleTokenReceiver for Contract { -// fn ft_on_transfer(&mut self, sender_id: AccountId, amount: U128, msg: String) -> PromiseOrValue { -// self.assert_from_ft_contract(); -// -// let ft_message: FtMessage = serde_json::from_str(&msg).expect("Unable to deserialize msg"); -// -// match ft_message { -// FtMessage::Stake(message) => { -// let receiver_id = message.receiver_id.unwrap_or(sender_id); -// self.deposit(receiver_id, message.ticket, amount, message.signature); -// } -// FtMessage::Migrate(jars) => { -// require!(sender_id == self.manager, "Migration can be performed only by admin"); -// -// self.migrate_jars(jars, amount); -// } -// } -// -// PromiseOrValue::Value(0.into()) -// } -// } +use near_contract_standards::fungible_token::receiver::FungibleTokenReceiver; +use near_sdk::{json_types::U128, near, require, serde_json, AccountId, PromiseOrValue}; +use sweat_jar_model::jar::CeFiJar; + +use crate::{jar::model::JarTicket, near_bindgen, Base64VecU8, Contract, ContractExt}; + +/// The `FtMessage` enum represents various commands for actions available via transferring tokens to an account +/// where this contract is deployed, using the payload in `ft_transfer_call`. +#[near(serializers=[json])] +#[serde(tag = "type", content = "data", rename_all = "snake_case")] +pub enum FtMessage { + /// Represents a request to create a new jar for a corresponding product. + Stake(StakeMessage), + + /// Represents a request to create `DeFi` Jars from provided `CeFi` Jars. + Migrate(Vec), +} + +/// The `StakeMessage` struct represents a request to create a new jar for a corresponding product. +#[near(serializers=[json])] +pub struct StakeMessage { + /// Data of the `JarTicket` required for validating the request and specifying the product. + ticket: JarTicket, + + /// An optional ed25519 signature used to verify the authenticity of the request. + signature: Option, + + /// An optional account ID representing the intended owner of the created jar. + receiver_id: Option, +} + +#[near_bindgen] +impl FungibleTokenReceiver for Contract { + fn ft_on_transfer(&mut self, sender_id: AccountId, amount: U128, msg: String) -> PromiseOrValue { + self.assert_from_ft_contract(); + + let ft_message: FtMessage = serde_json::from_str(&msg).expect("Unable to deserialize msg"); + + match ft_message { + FtMessage::Stake(message) => { + let receiver_id = message.receiver_id.unwrap_or(sender_id); + self.deposit(receiver_id, message.ticket, amount, message.signature); + } + FtMessage::Migrate(jars) => { + require!(sender_id == self.manager, "Migration can be performed only by admin"); + + self.migrate_jars(jars, amount); + } + } + + PromiseOrValue::Value(0.into()) + } +} // // #[cfg(test)] // mod tests { diff --git a/contract/src/internal.rs b/contract/src/internal.rs index b2b5015d..d0b19287 100644 --- a/contract/src/internal.rs +++ b/contract/src/internal.rs @@ -5,7 +5,10 @@ use sweat_jar_model::jar::JarId; use crate::{ env, - jar::{account::versioned::Account, model::Jar}, + jar::{ + account::versioned::Account, + model::{AccountJarsLegacy, Jar}, + }, AccountId, Contract, }; @@ -46,16 +49,18 @@ impl Contract { self.accounts.get(account_id).map(|record| record.jars.clone()) } - pub(crate) fn get_account_legacy(&self, account_id: &AccountId) -> Option<&Account> { + pub(crate) fn get_account_legacy(&self, account_id: &AccountId) -> Option { if let Some(record) = self.account_jars_v1.get(account_id) { - return Account::from(record).into(); + let account: Account = record.clone().into(); + return Some(account); } if let Some(record) = self.account_jars_non_versioned.get(account_id) { - return Account::from(record).into(); + let account: Account = record.clone().into(); + return Some(account); } - self.accounts.get(account_id) + self.accounts.get(account_id).cloned() } pub(crate) fn add_new_jar(&mut self, account_id: &AccountId, jar: Jar) { diff --git a/contract/src/jar/account/v1.rs b/contract/src/jar/account/v1.rs index 3abc613c..5fcf3bdd 100644 --- a/contract/src/jar/account/v1.rs +++ b/contract/src/jar/account/v1.rs @@ -10,7 +10,7 @@ use crate::{ }; #[near] -#[derive(Default, Debug, PartialEq)] +#[derive(Default, Debug, PartialEq, Clone)] pub struct AccountV1 { /// The last jar ID. Is used as nonce in `get_ticket_hash` method. pub last_id: JarId, diff --git a/contract/src/jar/account/versioned.rs b/contract/src/jar/account/versioned.rs index 1ee2976f..ae96175e 100644 --- a/contract/src/jar/account/versioned.rs +++ b/contract/src/jar/account/versioned.rs @@ -16,7 +16,7 @@ use crate::{ pub type Account = AccountVersioned; -#[derive(BorshSerialize, Debug, PartialEq)] +#[derive(BorshSerialize, Debug, PartialEq, Clone)] #[borsh(crate = "near_sdk::borsh")] pub enum AccountVersioned { V1(AccountV1), diff --git a/contract/src/jar/api.rs b/contract/src/jar/api.rs index bdd27333..fc6afe8e 100644 --- a/contract/src/jar/api.rs +++ b/contract/src/jar/api.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::{collections::HashMap, ops::Deref}; use near_sdk::{env, json_types::U128, near_bindgen, require, AccountId}; use sweat_jar_model::{ @@ -13,7 +13,7 @@ use crate::{ model::Deposit, view::DetailedJarV2, }, - product::model::v2::ProductV2, + product::model::v2::{InterestCalculator, ProductV2}, Contract, ContractExt, }; @@ -22,18 +22,43 @@ impl Contract { require!(product.is_enabled, "The product is disabled"); let account_id = env::predecessor_account_id(); - let account = self.get_account_mut(&account_id); - let jar = account.get_jar_mut(&product.id); - let now = env::block_timestamp_ms(); + + let account = self.get_account(&account_id); + let jar = account.get_jar(&product.id); + let (amount, partition_index) = jar.get_liquid_balance(&product.terms, now); require!(amount > 0, "Nothing to restake"); - self.update_jar_cache(account, &product.id); + self.update_jar_cache(&account_id, &product.id); + + let account = self.get_account_mut(&account_id); + let jar = account.get_jar_mut(&product.id); jar.clean_up_deposits(partition_index); account.deposit(&product.id, amount); } + + fn get_total_interest_for_account(&self, account: &AccountV2) -> AggregatedInterestView { + let mut detailed_amounts = HashMap::::new(); + let mut total_amount: TokenAmount = 0; + + for (product_id, jar) in account.jars.iter() { + let product = self.get_product(product_id); + let interest = product.terms.get_interest(account, &jar).0; + + detailed_amounts.insert(product_id.clone(), interest.into()); + total_amount += interest; + } + + AggregatedInterestView { + amount: AggregatedTokenAmountView { + detailed: detailed_amounts, + total: U128(total_amount), + }, + timestamp: env::block_timestamp_ms(), + } + } } #[near_bindgen] @@ -43,7 +68,11 @@ impl JarApi for Contract { return account .jars .iter() - .flat_map(|(product_id, jar)| DetailedJarV2(product_id.clone(), jar.clone()).into()) + .flat_map(|(product_id, jar)| { + let detailed_jar = &DetailedJarV2(product_id.clone(), jar.clone()); + let views: Vec = detailed_jar.into(); + views + }) .collect(); } @@ -54,14 +83,13 @@ impl JarApi for Contract { vec![] } - // TODO: add v2 support fn get_total_interest(&self, account_id: AccountId) -> AggregatedInterestView { if let Some(account) = self.try_get_account(&account_id) { - return account.get_total_interest(); + return self.get_total_interest_for_account(account); } if let Some(account) = self.get_account_legacy(&account_id) { - return AccountV2::from(account).get_total_interest(); + return self.get_total_interest_for_account(&AccountV2::from(account.deref())); } AggregatedInterestView::default() @@ -85,6 +113,7 @@ impl JarApi for Contract { .jars .keys() .filter(|product_id| self.get_product(product_id).is_enabled) + .cloned() .collect() }); for product_id in product_ids.iter() { @@ -105,29 +134,6 @@ impl JarApi for Contract { } } -impl AccountV2 { - fn get_total_interest(&self) -> AggregatedInterestView { - let mut detailed_amounts = HashMap::::new(); - let mut total_amount: TokenAmount = 0; - - for (product_id, jar) in self.jars { - let product = self.get_product(&product_id); - let interest = product.terms.get_interest(self, &jar).0; - - detailed_amounts.insert(product_id, interest.into()); - total_amount += interest; - } - - AggregatedInterestView { - amount: AggregatedTokenAmountView { - detailed: detailed_amounts, - total: U128(total_amount), - }, - timestamp: env::block_timestamp_ms(), - }; - } -} - impl From<&AccountV1> for AccountV2 { fn from(value: &AccountV1) -> Self { let mut account = AccountV2 { @@ -137,7 +143,7 @@ impl From<&AccountV1> for AccountV2 { is_penalty_applied: false, }; - for jar in value.jars { + for jar in value.jars.iter() { let deposit = Deposit::new(jar.created_at, jar.principal); account.push(&jar.product_id, deposit); diff --git a/contract/src/jar/model/common.rs b/contract/src/jar/model/common.rs index 601dafbf..5a113728 100644 --- a/contract/src/jar/model/common.rs +++ b/contract/src/jar/model/common.rs @@ -6,7 +6,7 @@ use near_sdk::{ use sweat_jar_model::{jar::JarId, Timezone, TokenAmount}; use crate::{ - common::Timestamp, jar::model::Jar, product::model::v2::Terms, score::AccountScore, Contract, JarsStorage, + common::Timestamp, jar::model::Jar, product::model::v2::Terms, Contract, JarsStorage, }; /// The `JarTicket` struct represents a request to create a deposit jar for a corresponding product. @@ -69,21 +69,6 @@ impl Contract { account.deposit(product_id, amount); } - pub(crate) fn get_score(&self, account: &AccountId) -> Option<&AccountScore> { - self.accounts.get(account).and_then(|a| a.score()) - } - - pub(crate) fn get_score_mut(&mut self, account: &AccountId) -> Option<&mut AccountScore> { - self.accounts.get_mut(account).and_then(|a| a.score_mut()) - } - - pub(crate) fn get_jar_mut_internal(&mut self, account: &AccountId, id: JarId) -> &mut Jar { - self.accounts - .get_mut(account) - .unwrap_or_else(|| env::panic_str(&format!("Account '{account}' doesn't exist"))) - .get_jar_mut(id) - } - #[mutants::skip] pub(crate) fn get_jar_internal(&self, account: &AccountId, id: JarId) -> Jar { if let Some(jars) = self.account_jars_v1.get(account) { diff --git a/contract/src/jar/model/v2.rs b/contract/src/jar/model/v2.rs index 4873c866..4d7c1ec0 100644 --- a/contract/src/jar/model/v2.rs +++ b/contract/src/jar/model/v2.rs @@ -44,8 +44,7 @@ impl JarV2 { (sum, partition_index) } else { - // TODO: add argument to `is_liquid` - let partition_index = self.deposits.partition_point(|deposit| deposit.is_liquid(now, todo!())); + let partition_index = self.deposits.partition_point(|deposit| terms.is_liquid(deposit)); let sum = self.deposits[..partition_index] .iter() diff --git a/contract/src/jar/view.rs b/contract/src/jar/view.rs index afc6e4ad..8b1f0b21 100644 --- a/contract/src/jar/view.rs +++ b/contract/src/jar/view.rs @@ -6,7 +6,7 @@ use crate::jar::model::{Jar, JarV2}; impl From for JarView { fn from(value: Jar) -> Self { Self { - id: value.id.into(), + id: value.id.to_string(), product_id: value.product_id.clone(), created_at: U64(value.created_at), principal: U128(value.principal), @@ -17,7 +17,7 @@ impl From for JarView { impl From<&Jar> for JarView { fn from(value: &Jar) -> Self { Self { - id: value.id.into(), + id: value.id.to_string(), product_id: value.product_id.clone(), created_at: U64(value.created_at), principal: U128(value.principal), @@ -35,8 +35,8 @@ impl From<&DetailedJarV2> for Vec { .deposits .iter() .map(|deposit| JarView { - product_id, - id: format!("{product_id}_{}", deposit.created_at), + id: format!("{}_{}", product_id.clone(), deposit.created_at), + product_id: product_id.clone(), created_at: deposit.created_at.into(), principal: deposit.principal.into(), }) diff --git a/contract/src/migration/account_jars_non_versioned.rs b/contract/src/migration/account_jars_non_versioned.rs index e4ff3cc2..51d6eaad 100644 --- a/contract/src/migration/account_jars_non_versioned.rs +++ b/contract/src/migration/account_jars_non_versioned.rs @@ -4,7 +4,7 @@ use sweat_jar_model::jar::JarId; use crate::jar::model::Jar; #[near] -#[derive(Default)] +#[derive(Default, Clone)] pub struct AccountJarsNonVersioned { pub last_id: JarId, pub jars: Vec, diff --git a/contract/src/migration/step_jars.rs b/contract/src/migration/step_jars.rs index 1b362135..84fd5872 100644 --- a/contract/src/migration/step_jars.rs +++ b/contract/src/migration/step_jars.rs @@ -1,13 +1,11 @@ -use std::collections::HashMap; - -use near_sdk::{collections::UnorderedMap, env, near, near_bindgen, store::LookupMap, AccountId, PanicOnDefault}; -use sweat_jar_model::{api::MigrationToStepJars, jar::JarId, ProductId}; +use near_sdk::{collections::UnorderedMap, near, store::LookupMap, AccountId, PanicOnDefault}; +use sweat_jar_model::{jar::JarId, ProductId}; use crate::{ jar::model::AccountJarsLegacy, migration::account_jars_non_versioned::AccountJarsNonVersioned, - product::model::v1::{Apy, Cap, Product, Terms, WithdrawalFee}, - Contract, ContractExt, StorageKey, + product::model::v1::{Apy, Cap, Terms, WithdrawalFee} + , }; #[near(serializers=[borsh, json])] @@ -34,45 +32,45 @@ pub struct ContractBeforeStepJars { pub account_jars_v1: LookupMap, } -#[near_bindgen] -impl MigrationToStepJars for Contract { - #[private] - #[init(ignore_state)] - #[mutants::skip] - fn migrate_state_to_step_jars() -> Self { - let mut old_state: ContractBeforeStepJars = env::state_read().expect("Failed to extract old contract state."); - - let mut products: near_sdk::collections::UnorderedMap = - near_sdk::collections::UnorderedMap::new(StorageKey::ProductsV2); - - for (product_id, product) in &old_state.products { - products.insert( - &product_id, - &Product { - id: product.id, - apy: product.apy, - cap: product.cap, - terms: product.terms, - withdrawal_fee: product.withdrawal_fee, - public_key: product.public_key, - is_enabled: product.is_enabled, - score_cap: 0, - }, - ); - } - - old_state.products.clear(); - - Contract { - token_account_id: old_state.token_account_id, - fee_account_id: old_state.fee_account_id, - manager: old_state.manager, - products, - last_jar_id: old_state.last_jar_id, - accounts: LookupMap::new(StorageKey::AccountsVersioned), - account_jars_non_versioned: LookupMap::new(StorageKey::AccountJarsV1), - account_jars_v1: LookupMap::new(StorageKey::AccountJarsLegacy), - products_cache: HashMap::default().into(), - } - } -} +// TODO: this migration will be outdated at the moment of release +// #[near_bindgen] +// impl MigrationToStepJars for Contract { +// #[private] +// #[init(ignore_state)] +// #[mutants::skip] +// fn migrate_state_to_step_jars() -> Self { +// let mut old_state: ContractBeforeStepJars = env::state_read().expect("Failed to extract old contract state."); +// +// let mut products: UnorderedMap = UnorderedMap::new(StorageKey::ProductsV2); +// +// for (product_id, product) in &old_state.products { +// products.insert( +// &product_id, +// &Product { +// id: product.id, +// apy: product.apy, +// cap: product.cap, +// terms: product.terms, +// withdrawal_fee: product.withdrawal_fee, +// public_key: product.public_key, +// is_enabled: product.is_enabled, +// score_cap: 0, +// }, +// ); +// } +// +// old_state.products.clear(); +// +// Contract { +// token_account_id: old_state.token_account_id, +// fee_account_id: old_state.fee_account_id, +// manager: old_state.manager, +// products, +// last_jar_id: old_state.last_jar_id, +// accounts: LookupMap::new(StorageKey::AccountsVersioned), +// account_jars_non_versioned: LookupMap::new(StorageKey::AccountJarsV1), +// account_jars_v1: LookupMap::new(StorageKey::AccountJarsLegacy), +// products_cache: HashMap::default().into(), +// } +// } +// } diff --git a/contract/src/penalty/api.rs b/contract/src/penalty/api.rs index 82aee5da..5db27bd8 100644 --- a/contract/src/penalty/api.rs +++ b/contract/src/penalty/api.rs @@ -16,10 +16,10 @@ impl PenaltyApi for Contract { self.assert_manager(); self.migrate_account_if_needed(&account_id); + self.update_account_cache(&account_id); let account = self.get_account_mut(&account_id); account.is_penalty_applied = value; - self.update_account_cache(account); emit(ApplyPenalty(PenaltyData { account_id, @@ -33,10 +33,10 @@ impl PenaltyApi for Contract { for account_id in account_ids.iter() { self.migrate_account_if_needed(&account_id); + self.update_account_cache(&account_id); let account = self.get_account_mut(account_id); account.is_penalty_applied = value; - self.update_account_cache(account); } emit(BatchApplyPenalty(BatchPenaltyData { diff --git a/contract/src/product/command.rs b/contract/src/product/dto.rs similarity index 100% rename from contract/src/product/command.rs rename to contract/src/product/dto.rs diff --git a/contract/src/product/mod.rs b/contract/src/product/mod.rs index 9e5d9c55..d90e9c7b 100644 --- a/contract/src/product/mod.rs +++ b/contract/src/product/mod.rs @@ -1,5 +1,5 @@ pub mod api; -pub mod command; +pub mod dto; pub mod helpers; pub mod model; pub mod tests; diff --git a/contract/src/product/model/v2.rs b/contract/src/product/model/v2.rs index 5abd8355..fc37d80d 100644 --- a/contract/src/product/model/v2.rs +++ b/contract/src/product/model/v2.rs @@ -130,13 +130,22 @@ impl Terms { _ => false, } } + + pub(crate) fn is_liquid(&self, deposit: &Deposit) -> bool { + let now = env::block_timestamp_ms(); + match self { + Terms::Fixed(terms) => deposit.is_liquid(now, terms.lockup_term), + Terms::Flexible(_) => true, + Terms::ScoreBased(terms) => deposit.is_liquid(now, terms.lockup_term), + } + } } impl ProductV2 { pub(crate) fn calculate_fee(&self, principal: TokenAmount) -> TokenAmount { if let Some(fee) = self.withdrawal_fee.clone() { return match fee { - WithdrawalFee::Fix(amount) => *amount, + WithdrawalFee::Fix(amount) => amount.clone(), WithdrawalFee::Percent(percent) => percent * principal, }; } @@ -197,8 +206,8 @@ pub(crate) trait InterestCalculator { (acc.0 + interest, acc.1 + remainder) }); - let remainder: u64 = (remainder % MS_IN_YEAR.into()).try_into().unwrap(); - let extra_interest = remainder / MS_IN_YEAR.into(); + let remainder: u64 = remainder % MS_IN_YEAR; + let extra_interest = (remainder / MS_IN_YEAR) as u128; (interest + extra_interest, remainder) } diff --git a/contract/src/score/score_api.rs b/contract/src/score/score_api.rs index 8e2f5ef3..ab835177 100644 --- a/contract/src/score/score_api.rs +++ b/contract/src/score/score_api.rs @@ -1,9 +1,8 @@ -use near_sdk::{env, env::block_timestamp_ms, near_bindgen, AccountId}; +use near_sdk::{near_bindgen, AccountId}; use sweat_jar_model::{api::ScoreApi, Score, U32, UTC}; use crate::{ event::{emit, EventKind, ScoreData}, - jar::model::JarCache, Contract, ContractExt, }; @@ -14,50 +13,24 @@ impl ScoreApi for Contract { let mut event = vec![]; - let now = block_timestamp_ms(); - - for (account, new_score) in batch { - self.migrate_account_if_needed(&account); - - let account_jars = self.accounts.entry(account.clone()).or_default(); - - assert!( - account_jars.has_score_jars(), - "Account '{account}' doesn't have score jars" - ); - - let score = account_jars.score.claim_score(); - - for jar in &mut account_jars.jars { - let product = self - .products - .get(&jar.product_id) - .unwrap_or_else(|| env::panic_str(&format!("Product '{}' doesn't exist", jar.product_id))); - - if !product.is_score_product() { - continue; - } - - let (interest, remainder) = jar.get_interest(&score, &product, now); - - jar.claim_remainder = remainder; + for (account_id, _) in batch.iter() { + self.migrate_account_if_needed(account_id); + self.update_account_cache(account_id); + } - jar.cache = Some(JarCache { - updated_at: now, - interest, - }); - } + for (account_id, new_score) in batch { + let account = self.get_account_mut(&account_id); - // Convert walkchain to user timezone + // Convert a record to user timezone let converted_score = new_score .iter() - .map(|score| (score.0, account_jars.score.timezone.adjust(score.1))) + .map(|score| (score.0, account.score.timezone.adjust(score.1))) .collect(); - account_jars.score.update(converted_score); + account.score.update(converted_score); event.push(ScoreData { - account_id: account, + account_id, score: new_score .into_iter() .map(|(score, timestamp)| (U32(score.into()), timestamp)) diff --git a/model/src/api.rs b/model/src/api.rs index d3486ce9..af55a3a6 100644 --- a/model/src/api.rs +++ b/model/src/api.rs @@ -1,7 +1,7 @@ #[cfg(not(feature = "integration-api"))] use near_sdk::{json_types::Base64VecU8, AccountId}; #[cfg(feature = "integration-api")] -use nitka::near_sdk::{json_types::Base64VecU8, AccountId}; +use nitka::near_sdk::*; use nitka_proc::make_integration_version; use crate::{ diff --git a/model/src/udecimal.rs b/model/src/udecimal.rs index c96a71e6..4bb36004 100644 --- a/model/src/udecimal.rs +++ b/model/src/udecimal.rs @@ -3,7 +3,7 @@ use std::{ ops::{Add, Mul}, }; -use near_sdk::near; +use near_sdk::{json_types::U128, near}; /// `UDecimal` represents a scientific representation of decimals. /// @@ -87,6 +87,12 @@ impl Add for UDecimal { } } +impl From<(U128, u32)> for UDecimal { + fn from(value: (U128, u32)) -> Self { + Self::new(value.0 .0, value.1) + } +} + #[cfg(test)] mod tests { use fake::Fake; diff --git a/res/sweat_jar.wasm b/res/sweat_jar.wasm index ee56f0527845b16b0dea36869dac0b2c19367130..d5287364b1f8da0dd20ddf026d798ea2d5c10970 100755 GIT binary patch literal 635959 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 LemTk?y(j)3^HheJ literal 619370 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