Skip to content

Commit

Permalink
feat: bulk restake and withdraw (#81)
Browse files Browse the repository at this point in the history
* feat: restake all

* feat: bulk withdraw method

* fix: rebase conflicts

* feat: catch panic in tests

* fix: integration tests

* fix: don't use accounts with magic numbers

* test: restake all unit tests

* test: withdraw all unit tests

* test: check deletion on withdraw all

* test: restake all integration test

* fix: near sdk 5 migration test

* test: withdraw all integration test

* test: failed bulk withdraw internal

* fix: optimised mutation tests

* fix: improved tests and mutation fix

* fix: optimised events to fit lots of jars

* fix: restake internal naming

* fix: review comments

* fix: improved restake internal

* fix: add gas assert to transfer_bulk_withdraw method
  • Loading branch information
VladasZ authored May 13, 2024
1 parent bba294b commit e0722b3
Show file tree
Hide file tree
Showing 52 changed files with 1,814 additions and 948 deletions.
249 changes: 140 additions & 109 deletions Cargo.lock

Large diffs are not rendered by default.

7 changes: 3 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,18 @@ fake = "2.8.0"
rand = "0.8.5"
futures = "0.3.28"
num-format = "0.4.4"
itertools = "0.12.1"
ed25519-dalek = { version = "2.0.0", features = ["rand_core"] }
base64 = "0.22.0"
sha256 = "1.3.0"
mutants = "0.0.3"
serde = "1.0"
sha2 = "0.10"

nitka = "0.3.0"
nitka-proc = "0.3.0"
nitka = "0.4.0"
nitka-proc = "0.4.0"

sweat-jar-model = { path = "model" }
sweat-model = { git = "https://github.com/sweatco/sweat-near", rev = "7fc49145026654404310b42efd0d20eb346e7ae2" }
sweat-model = { git = "https://github.com/sweatco/sweat-near", rev = "537ef7d0aa3bf58d87b77a1c9660b2d0299b6c00" }

near-workspaces = "0.10.0"
near-self-update-proc = "0.1.2"
Expand Down
2 changes: 1 addition & 1 deletion contract/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "sweat_jar"
version = "1.0.1"
version = "2.0.0"
authors = ["Sweat Economy"]
edition = "2021"

Expand Down
2 changes: 1 addition & 1 deletion contract/src/claim/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ impl Contract {
) -> PromiseOrValue<ClaimedAmountView> {
use crate::ft_interface::FungibleTokenInterface;
self.ft_contract()
.transfer(account_id, claimed_amount.get_total().0, "claim", &None)
.ft_transfer(account_id, claimed_amount.get_total().0, "claim", &None)
.then(after_claim_call(claimed_amount, jars_before_transfer, event, now))
.into()
}
Expand Down
116 changes: 52 additions & 64 deletions contract/src/claim/tests.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#![cfg(test)]

use near_sdk::{json_types::U128, test_utils::accounts, PromiseOrValue};
use near_sdk::{json_types::U128, test_utils::test_env::alice, PromiseOrValue};
use sweat_jar_model::{
api::{ClaimApi, JarApi, WithdrawApi},
claimed_amount_view::ClaimedAmountView,
Expand All @@ -11,31 +11,28 @@ use crate::{
common::{test_data::set_test_future_success, tests::Context, udecimal::UDecimal},
jar::model::Jar,
product::model::{Apy, Product},
test_utils::{admin, UnwrapPromise},
};

#[test]
fn claim_total_when_nothing_to_claim() {
let alice = accounts(0);
let admin = accounts(1);
let alice = alice();
let admin = admin();

let product = generate_product();
let jar = Jar::generate(0, &alice, &product.id).principal(100_000_000);
let mut context = Context::new(admin).with_products(&[product]).with_jars(&[jar]);

context.switch_account(&alice);
let result = context.contract.claim_total(None);

let PromiseOrValue::Value(value) = result else {
panic!();
};
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 = accounts(0);
let admin = accounts(1);
let alice = alice();
let admin = admin();

let product = generate_product();
let jar_0 = Jar::generate(0, &alice, &product.id).principal(100_000_000);
Expand All @@ -53,7 +50,7 @@ fn claim_total_detailed_when_having_tokens() {
context.set_block_timestamp_in_ms(test_duration);

context.switch_account(&alice);
let result = context.contract.claim_total(Some(true));
let result = context.contract().claim_total(Some(true));

let PromiseOrValue::Value(ClaimedAmountView::Detailed(value)) = result else {
panic!();
Expand All @@ -67,8 +64,8 @@ fn claim_total_detailed_when_having_tokens() {

#[test]
fn claim_partially_detailed_when_having_tokens() {
let alice = accounts(0);
let admin = accounts(1);
let alice = alice();
let admin = admin();

let product = generate_product();
let jar_0 = Jar::generate(0, &alice, &product.id).principal(100_000_000);
Expand All @@ -86,7 +83,7 @@ fn claim_partially_detailed_when_having_tokens() {
context.set_block_timestamp_in_ms(test_duration);

context.switch_account(&alice);
let result = context.contract.claim_jars(
let result = context.contract().claim_jars(
vec![U32(jar_0.id), U32(jar_1.id)],
Some(U128(jar_0_expected_interest + jar_1_expected_interest)),
Some(true),
Expand All @@ -104,8 +101,8 @@ fn claim_partially_detailed_when_having_tokens() {

#[test]
fn claim_pending_withdraw_jar() {
let alice = accounts(0);
let admin = accounts(1);
let alice = alice();
let admin = admin();

let product = generate_product();
let jar_0 = Jar::generate(0, &alice, &product.id).principal(100_000_000);
Expand All @@ -126,7 +123,7 @@ fn claim_pending_withdraw_jar() {
context.set_block_timestamp_in_ms(test_duration);

context.switch_account(&alice);
let result = context.contract.claim_jars(
let result = context.contract().claim_jars(
vec![U32(jar_0.id), U32(jar_1.id)],
Some(U128(jar_0_expected_interest.0 + jar_1_expected_interest)),
Some(true),
Expand All @@ -144,8 +141,8 @@ fn claim_pending_withdraw_jar() {

#[test]
fn claim_partially_detailed_when_having_tokens_and_request_sum_of_single_deposit() {
let alice = accounts(0);
let admin = accounts(1);
let alice = alice();
let admin = admin();

let product = generate_product();
let jar_0 = Jar::generate(0, &alice, &product.id).principal(100_000_000);
Expand All @@ -162,7 +159,7 @@ fn claim_partially_detailed_when_having_tokens_and_request_sum_of_single_deposit
context.set_block_timestamp_in_ms(test_duration);

context.switch_account(&alice);
let result = context.contract.claim_jars(
let result = context.contract().claim_jars(
vec![U32(jar_0.id), U32(jar_1.id)],
Some(U128(jar_0_expected_interest)),
Some(true),
Expand All @@ -180,8 +177,8 @@ fn claim_partially_detailed_when_having_tokens_and_request_sum_of_single_deposit

#[test]
fn claim_partially_when_having_tokens_to_claim() {
let alice = accounts(0);
let admin = accounts(1);
let alice = alice();
let admin = admin();

let product = generate_product();
let jar = Jar::generate(0, &alice, &product.id).principal(100_000_000_000);
Expand All @@ -190,20 +187,21 @@ 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![U32(jar.id)], Some(U128(100)), None) else {
panic!()
};
let claimed = context
.contract()
.claim_jars(vec![U32(jar.id)], Some(U128(100)), None)
.unwrap();

assert_eq!(claimed.get_total().0, 100);

let jar = context.contract.get_jar(alice, U32(jar.id));
let jar = context.contract().get_jar(alice, U32(jar.id));
assert_eq!(100, jar.claimed_balance.0);
}

#[test]
fn dont_delete_jar_on_all_interest_claim() {
let alice = accounts(0);
let admin = accounts(1);
let alice = alice();
let admin = admin();

let product = generate_product().apy(Apy::Constant(UDecimal::new(2, 1)));
let jar = Jar::generate(0, &alice, &product.id).principal(1_000_000);
Expand All @@ -213,10 +211,10 @@ fn dont_delete_jar_on_all_interest_claim() {

context.switch_account(&alice);
context
.contract
.contract()
.claim_jars(vec![U32(jar.id)], Some(U128(200_000)), None);

let jar = context.contract.get_jar_internal(&alice, jar.id);
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!() };
Expand All @@ -228,8 +226,8 @@ fn dont_delete_jar_on_all_interest_claim() {
#[test]
#[should_panic(expected = "Jar with id: 0 doesn't exist")]
fn claim_all_withdraw_all_and_delete_jar() {
let alice = accounts(0);
let admin = accounts(1);
let alice = alice();
let admin = admin();

let product = generate_product().apy(Apy::Constant(UDecimal::new(2, 1)));
let jar = Jar::generate(0, &alice, &product.id).principal(1_000_000);
Expand All @@ -243,38 +241,34 @@ fn claim_all_withdraw_all_and_delete_jar() {
context.set_block_timestamp_in_ms(product.get_lockup_term().unwrap() + 1);

context.switch_account(&alice);
let PromiseOrValue::Value(claimed) = context
.contract
let claimed = context
.contract()
.claim_jars(vec![U32(jar_id)], Some(U128(200_000)), None)
else {
panic!()
};
.unwrap();

assert_eq!(200_000, claimed.get_total().0);

let jar = context.contract.get_jar_internal(&alice, jar_id);
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 PromiseOrValue::Value(withdrawn) = context.contract.withdraw(U32(jar_id), None) else {
panic!()
};
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);
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 = accounts(0);
let admin = accounts(1);
let alice = alice();
let admin = admin();

let product = generate_product().apy(Apy::Constant(UDecimal::new(2, 1)));
let jar = Jar::generate(0, &alice, &product.id).principal(1_000_000);
Expand All @@ -289,35 +283,31 @@ fn withdraw_all_claim_all_and_delete_jar() {

context.switch_account(&alice);

let PromiseOrValue::Value(withdrawn) = context.contract.withdraw(U32(jar_id), None) else {
panic!()
};
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);
let jar = context.contract().get_jar_internal(&alice, jar_id);

assert_eq!(jar.principal, 0);

let PromiseOrValue::Value(claimed) = context
.contract
let claimed = context
.contract()
.claim_jars(vec![U32(jar_id)], Some(U128(200_000)), None)
else {
panic!();
};
.unwrap();

assert_eq!(claimed.get_total(), U128(200_000));

let _jar = context.contract.get_jar_internal(&alice, jar_id);
let _jar = context.contract().get_jar_internal(&alice, jar_id);
}

#[test]
fn failed_future_claim() {
set_test_future_success(false);

let alice = accounts(0);
let admin = accounts(1);
let alice = alice();
let admin = admin();

let product = generate_product().apy(Apy::Constant(UDecimal::new(2, 1)));
let jar = Jar::generate(0, &alice, &product.id).principal(1_000_000);
Expand All @@ -327,18 +317,16 @@ fn failed_future_claim() {

context.switch_account(&alice);

let jar_before_claim = context.contract.get_jar_internal(&alice, jar.id).clone();
let jar_before_claim = context.contract().get_jar_internal(&alice, jar.id).clone();

let PromiseOrValue::Value(claimed) = context
.contract
let claimed = context
.contract()
.claim_jars(vec![U32(jar.id)], Some(U128(200_000)), None)
else {
panic!()
};
.unwrap();

assert_eq!(claimed.get_total().0, 0);

let jar_after_claim = context.contract.get_jar_internal(&alice, jar.id);
let jar_after_claim = context.contract().get_jar_internal(&alice, jar.id);

assert_eq!(jar_before_claim, jar_after_claim);
}
Expand Down
13 changes: 12 additions & 1 deletion contract/src/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ pub type Duration = u64;
pub mod gas_data {
use near_sdk::Gas;

/// Const of `ft_transfer` call in token contract
pub(crate) const GAS_FOR_FT_TRANSFER: Gas = Gas::from_tgas(6);

/// Const of after claim call with 1 jar
const INITIAL_GAS_FOR_AFTER_CLAIM: Gas = Gas::from_tgas(4);

Expand All @@ -25,15 +28,23 @@ pub mod gas_data {
/// Value is measured with `measure_withdraw_test`
/// Average gas for this method call don't exceed 3.4 `TGas`. 4 here just in case.
pub(crate) const GAS_FOR_AFTER_WITHDRAW: Gas = Gas::from_tgas(4);

/// Value is measured with `measure_withdraw_all`
/// 10 `TGas` was enough for 200 jars. 15 here just in case.
pub(crate) const GAS_FOR_BULK_AFTER_WITHDRAW: Gas = Gas::from_tgas(15);
}

#[cfg(test)]
mod test {
use crate::common::gas_data::{GAS_FOR_AFTER_CLAIM, GAS_FOR_AFTER_WITHDRAW};
use crate::common::gas_data::{
GAS_FOR_AFTER_CLAIM, GAS_FOR_AFTER_WITHDRAW, GAS_FOR_BULK_AFTER_WITHDRAW, GAS_FOR_FT_TRANSFER,
};

#[test]
fn test_gas_methods() {
assert_eq!(GAS_FOR_FT_TRANSFER.as_gas(), 6_000_000_000_000);
assert_eq!(GAS_FOR_AFTER_CLAIM.as_gas(), 20_000_000_000_000);
assert_eq!(GAS_FOR_AFTER_WITHDRAW.as_gas(), 4_000_000_000_000);
assert_eq!(GAS_FOR_BULK_AFTER_WITHDRAW.as_gas(), 15_000_000_000_000);
}
}
Loading

0 comments on commit e0722b3

Please sign in to comment.