diff --git a/Cargo.lock b/Cargo.lock index 5cca92fb..fbbb2f57 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1410,7 +1410,9 @@ version = "1.0.0" dependencies = [ "anyhow", "async-trait", + "base64 0.21.3", "borsh 0.10.3", + "ed25519-dalek 2.0.0", "fake", "futures", "itertools", diff --git a/Cargo.toml b/Cargo.toml index 67a47265..a6b9e766 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,3 +11,6 @@ rand = "0.8.5" futures = "0.3.28" near-sdk = "4.1.1" itertools = "0.11.0" +ed25519-dalek = { version = "2.0.0", features = ["rand_core"] } +base64 = "0.21.3" +sha256 = "1.3.0" diff --git a/contract/Cargo.toml b/contract/Cargo.toml index 95f44af0..065b1686 100644 --- a/contract/Cargo.toml +++ b/contract/Cargo.toml @@ -11,12 +11,12 @@ crate-type = ["cdylib"] near-sdk = { workspace = true } near-contract-standards = "4.1.1" -ed25519-dalek = { version = "2.0.0", features = ["rand_core"] } +ed25519-dalek = { workspace = true } near-self-update = { git = "https://github.com/sweatco/near-self-update.git", rev = "7064db3cdd924efc7fa7c00664920a2b482e7bcf" } [dev-dependencies] fake = { workspace = true } rand = { workspace = true } -sha256 = "1.3.0" +sha256 = { workspace = true } crypto-hash = "0.3" -base64 = "0.21.3" +base64 = { workspace = true } diff --git a/contract/src/claim/api.rs b/contract/src/claim/api.rs index d4812687..9b646265 100644 --- a/contract/src/claim/api.rs +++ b/contract/src/claim/api.rs @@ -3,10 +3,10 @@ use std::cmp; use near_sdk::{env, ext_contract, is_promise_success, json_types::U128, near_bindgen, AccountId, PromiseOrValue}; use crate::{ - common::{TokenAmount, GAS_FOR_AFTER_CLAIM}, + common::{u32::U32, TokenAmount, GAS_FOR_AFTER_CLAIM}, event::{emit, ClaimEventItem, EventKind}, ft_interface::FungibleTokenInterface, - jar::model::{Jar, JarId}, + jar::{model::Jar, view::JarIdView}, Contract, ContractExt, Promise, }; @@ -34,7 +34,7 @@ pub trait ClaimApi { /// /// A `PromiseOrValue` representing the amount of tokens claimed. If the total available interest /// across the specified jars is zero or the provided `amount` is zero, the returned value will also be zero. - fn claim_jars(&mut self, jar_ids: Vec, amount: Option) -> PromiseOrValue; + fn claim_jars(&mut self, jar_ids: Vec, amount: Option) -> PromiseOrValue; } #[ext_contract(ext_self)] @@ -46,18 +46,18 @@ pub trait ClaimCallbacks { impl ClaimApi for Contract { fn claim_total(&mut self) -> PromiseOrValue { let account_id = env::predecessor_account_id(); - let jar_ids = self.account_jars(&account_id).iter().map(|a| a.id).collect(); + let jar_ids = self.account_jars(&account_id).iter().map(|a| U32(a.id)).collect(); self.claim_jars(jar_ids, None) } - fn claim_jars(&mut self, jar_ids: Vec, amount: Option) -> PromiseOrValue { + fn claim_jars(&mut self, jar_ids: Vec, amount: Option) -> PromiseOrValue { let account_id = env::predecessor_account_id(); let now = env::block_timestamp_ms(); let unlocked_jars: Vec = self .account_jars(&account_id) .iter() - .filter(|jar| !jar.is_pending_withdraw && jar_ids.contains(&jar.id)) + .filter(|jar| !jar.is_pending_withdraw && jar_ids.contains(&U32(jar.id))) .cloned() .collect(); diff --git a/contract/src/claim/tests.rs b/contract/src/claim/tests.rs index d8098d0a..60d8bbb7 100644 --- a/contract/src/claim/tests.rs +++ b/contract/src/claim/tests.rs @@ -41,7 +41,7 @@ fn claim_partially_when_having_tokens_to_claim() { context.set_block_timestamp_in_days(365); context.switch_account(&alice); - let PromiseOrValue::Value(claimed) = context.contract.claim_jars(vec![jar.id], Some(U128(100))) else { + let PromiseOrValue::Value(claimed) = context.contract.claim_jars(vec![U32(jar.id)], Some(U128(100))) else { panic!() }; @@ -63,7 +63,7 @@ fn dont_delete_jar_on_all_interest_claim() { context.set_block_timestamp_in_days(365); context.switch_account(&alice); - context.contract.claim_jars(vec![jar.id], Some(U128(200_000))); + context.contract.claim_jars(vec![U32(jar.id)], Some(U128(200_000))); let jar = context.contract.get_jar_internal(&alice, jar.id); assert_eq!(200_000, jar.claimed_balance); @@ -92,7 +92,7 @@ fn claim_all_withdraw_all_and_delete_jar() { context.set_block_timestamp_in_ms(product.get_lockup_term().unwrap() + 1); context.switch_account(&alice); - context.contract.claim_jars(vec![jar_id], Some(U128(200_000))); + context.contract.claim_jars(vec![U32(jar_id)], Some(U128(200_000))); let jar = context.contract.get_jar_internal(&alice, jar_id); assert_eq!(200_000, jar.claimed_balance); @@ -140,7 +140,7 @@ fn withdraw_all_claim_all_and_delete_jar() { let jar = context.contract.get_jar_internal(&alice, jar_id); - let PromiseOrValue::Value(claimed) = context.contract.claim_jars(vec![jar_id], Some(U128(200_000))) else { + let PromiseOrValue::Value(claimed) = context.contract.claim_jars(vec![U32(jar_id)], Some(U128(200_000))) else { panic!(); }; diff --git a/contract/src/penalty/api.rs b/contract/src/penalty/api.rs index 9d64ece4..a9b82c56 100644 --- a/contract/src/penalty/api.rs +++ b/contract/src/penalty/api.rs @@ -2,7 +2,7 @@ use near_sdk::{env, near_bindgen, AccountId}; use crate::{ event::{emit, EventKind::ApplyPenalty, PenaltyData}, - jar::model::JarId, + jar::view::JarIdView, product::model::Apy, Contract, ContractExt, }; @@ -23,14 +23,15 @@ 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: JarId, value: bool); + fn set_penalty(&mut self, account_id: AccountId, jar_id: JarIdView, value: bool); } #[near_bindgen] impl PenaltyApi for Contract { - fn set_penalty(&mut self, account_id: AccountId, jar_id: JarId, value: bool) { + fn set_penalty(&mut self, account_id: AccountId, jar_id: JarIdView, value: bool) { self.assert_manager(); + let jar_id = jar_id.0; let jar = self.get_jar_internal(&account_id, jar_id); let product = self.get_product(&jar.product_id); diff --git a/contract/src/product/api.rs b/contract/src/product/api.rs index 7a2b438d..1d681b0c 100644 --- a/contract/src/product/api.rs +++ b/contract/src/product/api.rs @@ -72,7 +72,6 @@ impl ProductApi for Contract { emit(EventKind::RegisterProduct(product)); } - // TODO: add integration tests #[payable] fn set_enabled(&mut self, product_id: ProductId, is_enabled: bool) { self.assert_manager(); @@ -90,7 +89,6 @@ impl ProductApi for Contract { })); } - // TODO: add integration tests #[payable] fn set_public_key(&mut self, product_id: ProductId, public_key: Base64VecU8) { self.assert_manager(); diff --git a/contract/src/tests.rs b/contract/src/tests.rs index f2ddda4c..11b66cad 100644 --- a/contract/src/tests.rs +++ b/contract/src/tests.rs @@ -223,7 +223,7 @@ fn penalty_is_not_applicable_for_constant_apy() { .with_jars(&[reference_jar]); context.switch_account(&admin); - context.contract.set_penalty(alice, 0, true); + context.contract.set_penalty(alice, U32(0), true); } #[test] @@ -251,7 +251,7 @@ fn get_total_interest_for_premium_with_penalty_after_half_term() { assert_eq!(interest, 9_972_602); context.switch_account(&admin); - context.contract.set_penalty(alice.clone(), 0, true); + context.contract.set_penalty(alice.clone(), U32(0), true); context.set_block_timestamp_in_days(365); diff --git a/docs/requirements.md b/docs/requirements.md index 9bc5fa4d..ef372e91 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -165,14 +165,17 @@ The `ft_interface.rs` file contains helpers to facilitate interaction with the r The code in `ft_receiver.rs` handles incoming Token transfers. This mechanism is used for Jar creation, top-ups, and migration. -#### 3.1.4. ๐Ÿงช Integration tests +#### 3.1.4. ๐ŸŒก๏ธ Integration tests The `./integration-tests` directory contains integration tests for the smart contract. These tests work with both FT and DeFi Jars contracts, covering the following scenarios: -- **happy_flow.rs:** This scenario represents the successful registration of a product, the creation of a Jar, and the accrual of interest. -- **migration.rs:** These tests focus on the batched migration of CeFi $SWEAT Jars to the contract. -- **withdraw_fee.rs:** These tests deal with withdrawing Jars with fees, checking for the correct transfer of principal to a user account and fees account. +- **[happy_flow.rs](..%2Fintegration-tests%2Fsrc%2Fhappy_flow.rs):** This scenario represents the successful registration of a product, the creation of a Jar, and the accrual of interest. +- **[migration.rs](..%2Fintegration-tests%2Fsrc%2Fmigration.rs):** These tests focus on the batched migration of CeFi $SWEAT Jars to the contract. +- **[premium_product.rs](..%2Fintegration-tests%2Fsrc%2Fpremium_product.rs):** These tests cover mechanics related to Premium Products, including creation with a signature and applying penalties. +- **[product_actions.rs](..%2Fintegration-tests%2Fsrc%2Fproduct_actions.rs):** These tests confirm operations on Products that can be performed by an Admin. +- **[restake.rs](..%2Fintegration-tests%2Fsrc%2Frestake.rs):** These tests examine the creation of a new Jar from another Jar and the cleanup process after claiming from the original Jar. +- **[withdraw_fee.rs](..%2Fintegration-tests%2Fsrc%2Fwithdraw_fee.rs):** These tests deal with withdrawing Jars with fees, checking for the correct transfer of principal to a user account and fees account. In addition to these files, it also contains utilities and testing data, with the most significant being: diff --git a/integration-tests/Cargo.toml b/integration-tests/Cargo.toml index 65791475..812cded6 100644 --- a/integration-tests/Cargo.toml +++ b/integration-tests/Cargo.toml @@ -13,6 +13,8 @@ rand = { workspace = true } futures = { workspace = true } near-sdk = { workspace = true } itertools = { workspace = true } +ed25519-dalek = { workspace = true } +base64 = { workspace = true } anyhow = "1.0" borsh = "0.10.3" diff --git a/integration-tests/src/common.rs b/integration-tests/src/common.rs index 22089e44..d4597313 100644 --- a/integration-tests/src/common.rs +++ b/integration-tests/src/common.rs @@ -3,6 +3,8 @@ use std::{ sync::atomic::{AtomicBool, Ordering}, }; +use ed25519_dalek::{SigningKey, VerifyingKey}; +use rand::rngs::OsRng; use serde_json::Value; use workspaces::Account; @@ -11,6 +13,7 @@ use crate::{context::Context, product::RegisterProductCommand}; pub trait ValueGetters { fn get_u128(&self, key: &str) -> u128; fn get_interest(&self) -> u128; + fn get_jar_id(&self) -> String; } impl ValueGetters for Value { @@ -29,6 +32,16 @@ impl ValueGetters for Value { fn get_interest(&self) -> u128 { self.as_object().unwrap().get("amount").unwrap().get_u128("total") } + + fn get_jar_id(&self) -> String { + self.as_object() + .unwrap() + .get("id") + .unwrap() + .as_str() + .unwrap() + .to_string() + } } static CONTRACT_READY: AtomicBool = AtomicBool::new(false); @@ -93,3 +106,11 @@ pub(crate) async fn prepare_contract( fee_account, }) } + +pub(crate) fn generate_keypair() -> (SigningKey, VerifyingKey) { + let mut csprng = OsRng; + let signing_key: SigningKey = SigningKey::generate(&mut csprng); + let verifying_key: VerifyingKey = VerifyingKey::from(&signing_key); + + (signing_key, verifying_key) +} diff --git a/integration-tests/src/context.rs b/integration-tests/src/context.rs index dc0e65ab..02c8e396 100644 --- a/integration-tests/src/context.rs +++ b/integration-tests/src/context.rs @@ -74,4 +74,23 @@ impl Context { Ok(()) } + + pub(crate) fn get_signature_material( + &self, + receiver_id: &Account, + product_id: &String, + valid_until: u64, + amount: u128, + last_jar_id: Option, + ) -> String { + format!( + "{},{},{},{},{},{}", + self.jar_contract.account().id(), + receiver_id.id(), + product_id, + amount, + last_jar_id.map_or_else(String::new, |value| value,), + valid_until, + ) + } } diff --git a/integration-tests/src/jar_contract_interface.rs b/integration-tests/src/jar_contract_interface.rs index d7c8b2ef..49abee82 100644 --- a/integration-tests/src/jar_contract_interface.rs +++ b/integration-tests/src/jar_contract_interface.rs @@ -1,4 +1,5 @@ use async_trait::async_trait; +use near_sdk::json_types::U128; use near_units::parse_near; use serde_json::{json, Value}; use workspaces::{Account, AccountId, Contract}; @@ -26,17 +27,43 @@ pub(crate) trait JarContractInterface { product_id: String, amount: u128, ft_contract_id: &AccountId, - ) -> anyhow::Result<()>; + ) -> anyhow::Result; + + async fn create_premium_jar( + &self, + user: &Account, + product_id: String, + amount: u128, + signature: String, + valid_until: u64, + ft_contract_id: &AccountId, + ) -> anyhow::Result; async fn get_total_principal(&self, user: &Account) -> anyhow::Result; + async fn get_principal(&self, user: &Account, jar_ids: Vec) -> anyhow::Result; + async fn get_total_interest(&self, user: &Account) -> anyhow::Result; + async fn get_interest(&self, user: &Account, jar_ids: Vec) -> anyhow::Result; + async fn get_jars_for_account(&self, user: &Account) -> anyhow::Result; async fn withdraw(&self, user: &Account, jar_id: &str) -> anyhow::Result; async fn claim_total(&self, user: &Account) -> anyhow::Result; + + async fn claim_jars(&self, user: &Account, jar_ids: Vec, amount: Option) -> anyhow::Result; + + async fn get_jar(&self, account_id: String, jar_id: String) -> anyhow::Result; + + async fn restake(&self, user: &Account, jar_id: String) -> anyhow::Result<()>; + + async fn set_penalty(&self, admin: &Account, account_id: &str, jar_id: &str, value: bool) -> anyhow::Result<()>; + + async fn set_enabled(&self, admin: &Account, product_id: String, is_enabled: bool) -> anyhow::Result<()>; + + async fn set_public_key(&self, admin: &Account, product_id: String, public_key: String) -> anyhow::Result<()>; } #[async_trait] @@ -107,7 +134,7 @@ impl JarContractInterface for Contract { product_id: String, amount: u128, ft_contract_id: &AccountId, - ) -> anyhow::Result<()> { + ) -> anyhow::Result { println!( "โ–ถ๏ธ Create jar(product = {:?}) for user {:?} with {:?} tokens", product_id, @@ -125,26 +152,37 @@ impl JarContractInterface for Contract { } }); - let args = json!({ - "receiver_id": self.as_account().id(), - "amount": amount.to_string(), - "msg": msg.to_string(), - }); + self.create_jar_internal(user, msg, amount, ft_contract_id).await + } - let result = user - .call(ft_contract_id, "ft_transfer_call") - .args_json(args) - .max_gas() - .deposit(parse_near!("1 yocto")) - .transact() - .await? - .into_result()?; + async fn create_premium_jar( + &self, + user: &Account, + product_id: String, + amount: u128, + signature: String, + valid_until: u64, + ft_contract_id: &AccountId, + ) -> anyhow::Result { + println!( + "โ–ถ๏ธ Create premium jar(product = {:?}) for user {:?} with {:?} tokens", + product_id, + user.id(), + amount + ); - for log in result.logs() { - println!(" ๐Ÿ“– {log}"); - } + let msg = json!({ + "type": "stake", + "data": { + "ticket": { + "product_id": product_id, + "valid_until": valid_until.to_string(), + }, + "signature": signature, + } + }); - Ok(()) + self.create_jar_internal(user, msg, amount, ft_contract_id).await } async fn get_total_principal(&self, user: &Account) -> anyhow::Result { @@ -161,6 +199,21 @@ impl JarContractInterface for Contract { Ok(result) } + async fn get_principal(&self, user: &Account, jar_ids: Vec) -> anyhow::Result { + println!("โ–ถ๏ธ Get principal for jars {:?}", jar_ids); + + let args = json!({ + "account_id": user.id(), + "jar_ids": jar_ids, + }); + + let result = self.view("get_principal").args_json(args).await?.json()?; + + println!(" โœ… {:?}", result); + + Ok(result) + } + async fn get_total_interest(&self, user: &Account) -> anyhow::Result { println!("โ–ถ๏ธ Get total interest for user {:?}", user.id()); @@ -175,6 +228,21 @@ impl JarContractInterface for Contract { Ok(result) } + async fn get_interest(&self, user: &Account, jar_ids: Vec) -> anyhow::Result { + println!("โ–ถ๏ธ Get interest for jars {:?}", jar_ids); + + let args = json!({ + "account_id": user.id(), + "jar_ids": jar_ids, + }); + + let result = self.view("get_interest").args_json(args).await?.json()?; + + println!(" โœ… {:?}", result); + + Ok(result) + } + async fn get_jars_for_account(&self, user: &Account) -> anyhow::Result { println!("โ–ถ๏ธ Get jars for user {:?}", user.id()); @@ -246,4 +314,199 @@ impl JarContractInterface for Contract { Ok(result_value.as_str().unwrap().to_string().parse::()?) } + + async fn claim_jars(&self, user: &Account, jar_ids: Vec, amount: Option) -> anyhow::Result { + println!("โ–ถ๏ธ Claim jars: {:?}", jar_ids); + + let args = json!({ + "jar_ids": jar_ids, + "amount": amount, + }); + + let result = user + .call(self.id(), "claim_jars") + .args_json(args) + .max_gas() + .transact() + .await? + .into_result()?; + + for log in result.logs() { + println!(" ๐Ÿ“– {log}"); + } + + println!(" ๐Ÿ“Ÿ {result:#?}"); + + let result_value = result.json::()?; + + println!(" โœ… {result_value:?}"); + + OutcomeStorage::add_result(result); + + Ok(result_value.as_str().unwrap().to_string().parse::()?) + } + + async fn get_jar(&self, account_id: String, jar_id: String) -> anyhow::Result { + println!("โ–ถ๏ธ Get jar #{jar_id}"); + + let args = json!({ + "account_id": account_id, + "jar_id": jar_id, + }); + + let result = self.view("get_jar").args_json(args).await?.json()?; + + println!(" โœ… {:?}", result); + + Ok(result) + } + + async fn restake(&self, user: &Account, jar_id: String) -> anyhow::Result<()> { + println!("โ–ถ๏ธ Restake jar #{jar_id}"); + + let args = json!({ + "jar_id": jar_id, + }); + + let result = user + .call(self.id(), "restake") + .args_json(args) + .max_gas() + .transact() + .await? + .into_result()?; + + for log in result.logs() { + println!(" ๐Ÿ“– {log}"); + } + + Ok(()) + } + + async fn set_penalty(&self, admin: &Account, account_id: &str, jar_id: &str, value: bool) -> anyhow::Result<()> { + println!("โ–ถ๏ธ Set penalty for jar #{jar_id}"); + + let args = json!({ + "account_id": account_id, + "jar_id": jar_id, + "value": value, + }); + + let result = admin + .call(self.id(), "set_penalty") + .args_json(args) + .max_gas() + .transact() + .await? + .into_result()?; + + for log in result.logs() { + println!(" ๐Ÿ“– {log}"); + } + + Ok(()) + } + + async fn set_enabled(&self, admin: &Account, product_id: String, is_enabled: bool) -> anyhow::Result<()> { + println!("โ–ถ๏ธ Set enabled for product #{product_id}"); + + let args = json!({ + "product_id": product_id, + "is_enabled": is_enabled, + }); + + let result = admin + .call(self.id(), "set_enabled") + .args_json(args) + .max_gas() + .deposit(parse_near!("1 yocto")) + .transact() + .await? + .into_result()?; + + for log in result.logs() { + println!(" ๐Ÿ“– {log}"); + } + + Ok(()) + } + + async fn set_public_key(&self, admin: &Account, product_id: String, public_key: String) -> anyhow::Result<()> { + println!("โ–ถ๏ธ Set public key for product #{product_id}: {public_key}"); + + let args = json!({ + "product_id": product_id, + "public_key": public_key, + }); + + println!("Args: {:?}", args); + + let result = admin + .call(self.id(), "set_public_key") + .args_json(args) + .max_gas() + .deposit(parse_near!("1 yocto")) + .transact() + .await? + .into_result()?; + + for log in result.logs() { + println!(" ๐Ÿ“– {log}"); + } + + Ok(()) + } +} + +#[async_trait] +trait Internal { + async fn create_jar_internal( + &self, + user: &Account, + msg: Value, + amount: u128, + ft_contract_id: &AccountId, + ) -> anyhow::Result; +} + +#[async_trait] +impl Internal for Contract { + async fn create_jar_internal( + &self, + user: &Account, + msg: Value, + amount: u128, + ft_contract_id: &AccountId, + ) -> anyhow::Result { + println!("โ–ถ๏ธ Create jar with msg: {:?}", msg,); + + let args = json!({ + "receiver_id": self.as_account().id(), + "amount": amount.to_string(), + "msg": msg.to_string(), + }); + + let result = user + .call(ft_contract_id, "ft_transfer_call") + .args_json(args) + .max_gas() + .deposit(parse_near!("1 yocto")) + .transact() + .await?; + + for log in result.logs() { + println!(" ๐Ÿ“– {log}"); + } + + for failure in result.failures() { + println!(" โŒ {:?}", failure); + } + + if let Some(failure) = result.failures().into_iter().next().cloned() { + let error = failure.into_result().err().unwrap(); + return Err(error.into()); + } + + Ok(result.json()?) + } } diff --git a/integration-tests/src/lib.rs b/integration-tests/src/lib.rs index d8b3728a..8be79467 100644 --- a/integration-tests/src/lib.rs +++ b/integration-tests/src/lib.rs @@ -7,5 +7,8 @@ mod happy_flow; mod jar_contract_interface; mod measure; mod migration; +mod premium_product; mod product; +mod product_actions; +mod restake; mod withdraw_fee; diff --git a/integration-tests/src/premium_product.rs b/integration-tests/src/premium_product.rs new file mode 100644 index 00000000..d837b447 --- /dev/null +++ b/integration-tests/src/premium_product.rs @@ -0,0 +1,122 @@ +use base64::{engine::general_purpose::STANDARD, Engine}; +use ed25519_dalek::Signer; +use near_sdk::env::sha256; + +use crate::{ + common::{generate_keypair, prepare_contract, Prepared, ValueGetters}, + product::RegisterProductCommand, +}; + +#[tokio::test] +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 Prepared { + context, + manager, + alice, + fee_account: _, + } = prepare_contract([]).await?; + + let register_product_command = RegisterProductCommand::Flexible6Months6Percents; + let command_json = register_product_command.json_for_premium(pk_base64); + + context.jar_contract.register_product(&manager, command_json).await?; + + let product_id = register_product_command.id(); + let valid_until = 43_012_170_000_000; + let amount = 3_000_000; + + let signature = STANDARD.encode( + signing_key + .sign( + sha256( + context + .get_signature_material(&alice, &product_id, valid_until, amount, None) + .as_bytes(), + ) + .as_slice(), + ) + .to_bytes(), + ); + + let result = context + .jar_contract + .create_premium_jar( + &alice, + product_id.clone(), + amount, + signature.to_string(), + valid_until, + context.ft_contract.account().id(), + ) + .await?; + + assert_eq!(result.as_str().unwrap(), amount.to_string()); + + let jars = context.jar_contract.get_jars_for_account(&alice).await?; + let jar_id = jars.as_array().unwrap().get(0).unwrap().get_jar_id(); + + let jar = context + .jar_contract + .get_jar(alice.id().to_string(), jar_id.clone()) + .await?; + + assert_eq!( + jar.as_object().unwrap().get("principal").unwrap().as_str().unwrap(), + amount.to_string() + ); + assert!(!jar + .as_object() + .unwrap() + .get("is_penalty_applied") + .unwrap() + .as_bool() + .unwrap()); + + context + .jar_contract + .set_penalty(&manager, alice.id(), &jar_id.clone(), true) + .await?; + + let jar = context + .jar_contract + .get_jar(alice.id().to_string(), jar_id.clone()) + .await?; + + assert!(jar + .as_object() + .unwrap() + .get("is_penalty_applied") + .unwrap() + .as_bool() + .unwrap()); + + let unauthorized_penalty_change = context + .jar_contract + .set_penalty(&alice, alice.id(), &jar_id.clone(), true) + .await; + + assert!(unauthorized_penalty_change.is_err()); + + let principal_result = context.jar_contract.get_principal(&alice, vec![jar_id.clone()]).await?; + assert_eq!( + principal_result + .as_object() + .unwrap() + .get("total") + .unwrap() + .as_str() + .unwrap() + .to_string(), + amount.to_string() + ); + + let interest_result = context.jar_contract.get_interest(&alice, vec![jar_id]).await; + assert!(interest_result.is_ok()); + + Ok(()) +} diff --git a/integration-tests/src/product.rs b/integration-tests/src/product.rs index ddccd5a7..2d6f523b 100644 --- a/integration-tests/src/product.rs +++ b/integration-tests/src/product.rs @@ -4,17 +4,21 @@ use serde_json::{json, Value}; pub(crate) enum RegisterProductCommand { Locked12Months12Percents, Locked6Months6Percents, + Flexible6Months6Percents, Locked6Months6PercentsWithWithdrawFee, + Locked10Minutes6Percents, Locked10Minutes6PercentsWithFixedWithdrawFee, Locked10Minutes6PercentsWithPercentWithdrawFee, } impl RegisterProductCommand { - pub(crate) fn all() -> [Self; 5] { + pub(crate) fn all() -> [Self; 7] { [ Self::Locked12Months12Percents, Self::Locked6Months6Percents, + Self::Flexible6Months6Percents, Self::Locked6Months6PercentsWithWithdrawFee, + Self::Locked10Minutes6Percents, Self::Locked10Minutes6PercentsWithFixedWithdrawFee, Self::Locked10Minutes6PercentsWithPercentWithdrawFee, ] @@ -22,6 +26,14 @@ impl RegisterProductCommand { } impl RegisterProductCommand { + pub(crate) fn json_for_premium(&self, public_key: String) -> Value { + let mut json = self.json(); + if let Value::Object(obj) = &mut json { + obj.insert("public_key".to_string(), Value::String(public_key)); + } + json + } + pub(crate) fn json(&self) -> Value { match self { RegisterProductCommand::Locked12Months12Percents => json!({ @@ -54,6 +66,17 @@ impl RegisterProductCommand { }, "is_enabled": true, }), + RegisterProductCommand::Flexible6Months6Percents => json!({ + "id": "flexible_6_months_6_percents", + "apy_default": ["12", 2], + "apy_fallback": ["6", 2], + "cap_min": "100000", + "cap_max": "100000000000", + "terms": { + "type": "flexible", + }, + "is_enabled": true, + }), RegisterProductCommand::Locked6Months6PercentsWithWithdrawFee => json!({ "id": "locked_6_months_6_percents_with_withdraw_fee", "apy_default": ["6", 2], @@ -73,6 +96,21 @@ impl RegisterProductCommand { }, "is_enabled": true, }), + RegisterProductCommand::Locked10Minutes6Percents => json!({ + "id": "locked_10_minutes_6_percents", + "apy_default": ["6", 2], + "cap_min": "100000", + "cap_max": "100000000000", + "terms": { + "type": "fixed", + "data": { + "lockup_term": "600000", + "allows_top_up": false, + "allows_restaking": true, + } + }, + "is_enabled": true, + }), RegisterProductCommand::Locked10Minutes6PercentsWithFixedWithdrawFee => json!({ "id": "locked_10_minutes_6_percents_with_fixed_withdraw_fee", "apy_default": ["6", 2], diff --git a/integration-tests/src/product_actions.rs b/integration-tests/src/product_actions.rs new file mode 100644 index 00000000..161cb690 --- /dev/null +++ b/integration-tests/src/product_actions.rs @@ -0,0 +1,87 @@ +use base64::{engine::general_purpose::STANDARD, Engine}; + +use crate::{ + common::{generate_keypair, prepare_contract, Prepared}, + product::RegisterProductCommand, +}; + +#[tokio::test] +async fn product_actions() -> anyhow::Result<()> { + println!("๐Ÿ‘ท๐Ÿฝ Run test for product actions"); + + let Prepared { + context, + manager, + alice, + fee_account: _, + } = prepare_contract([RegisterProductCommand::Locked12Months12Percents]).await?; + + let product_id = RegisterProductCommand::Locked12Months12Percents.id(); + + let result = context + .jar_contract + .create_jar( + &alice, + product_id.clone(), + 1_000_000, + context.ft_contract.account().id(), + ) + .await?; + + assert_eq!(result.as_str().unwrap(), "1000000"); + + context + .jar_contract + .set_enabled(&manager, RegisterProductCommand::Locked12Months12Percents.id(), false) + .await?; + + let result = context + .jar_contract + .create_jar( + &alice, + product_id.clone(), + 1_000_000, + context.ft_contract.account().id(), + ) + .await; + + assert!(result.is_err()); + assert!(result + .err() + .unwrap() + .root_cause() + .to_string() + .contains("It's not possible to create new jars for this product")); + + context + .jar_contract + .set_enabled(&manager, RegisterProductCommand::Locked12Months12Percents.id(), true) + .await?; + + let (_, verifying_key) = generate_keypair(); + let pk_base64 = STANDARD.encode(verifying_key.as_bytes()); + + context + .jar_contract + .set_public_key( + &manager, + RegisterProductCommand::Locked12Months12Percents.id(), + pk_base64, + ) + .await?; + + let result = context + .jar_contract + .create_jar(&alice, product_id, 1_000_000, context.ft_contract.account().id()) + .await; + + assert!(result.is_err()); + assert!(result + .err() + .unwrap() + .root_cause() + .to_string() + .contains("Signature is required")); + + Ok(()) +} diff --git a/integration-tests/src/restake.rs b/integration-tests/src/restake.rs new file mode 100644 index 00000000..4604ae47 --- /dev/null +++ b/integration-tests/src/restake.rs @@ -0,0 +1,64 @@ +use crate::{ + common::{prepare_contract, Prepared, ValueGetters}, + product::RegisterProductCommand, +}; + +#[tokio::test] +async fn restake() -> anyhow::Result<()> { + println!("๐Ÿ‘ท๐Ÿฝ Run test for restaking"); + + let product_command = RegisterProductCommand::Locked10Minutes6Percents; + let product_id = product_command.id(); + + let Prepared { + context, + manager: _, + alice, + fee_account: _, + } = prepare_contract([product_command]).await?; + + let amount = 1_000_000; + context + .jar_contract + .create_jar(&alice, product_id, amount, context.ft_contract.account().id()) + .await?; + + let jars = context.jar_contract.get_jars_for_account(&alice).await?; + let original_jar_id = jars.as_array().unwrap().get(0).unwrap().get_jar_id(); + + context.fast_forward_hours(1).await?; + + context.jar_contract.restake(&alice, original_jar_id.clone()).await?; + + let jars = context.jar_contract.get_jars_for_account(&alice).await?; + let jars_array = jars.as_array().unwrap(); + assert_eq!(jars_array.len(), 2); + + let mut has_original_jar = false; + let mut has_restaked_jar = false; + for jar in jars_array { + let id = jar.get_jar_id(); + + if id == original_jar_id { + has_original_jar = true; + assert_eq!(jar.get("principal").unwrap().as_str().unwrap(), "0"); + } else { + has_restaked_jar = true; + assert_eq!(jar.get("principal").unwrap().as_str().unwrap(), amount.to_string()); + } + } + + assert!(has_original_jar); + assert!(has_restaked_jar); + + context + .jar_contract + .claim_jars(&alice, vec![original_jar_id], None) + .await?; + + let jars = context.jar_contract.get_jars_for_account(&alice).await?; + let jars_array = jars.as_array().unwrap(); + assert_eq!(jars_array.len(), 1); + + Ok(()) +} diff --git a/res/sweat_jar.wasm b/res/sweat_jar.wasm index 584627a3..fa239aca 100755 Binary files a/res/sweat_jar.wasm and b/res/sweat_jar.wasm differ