Skip to content

Commit 96fc4a7

Browse files
committed
feat: testing with score timestamp
1 parent d1730fa commit 96fc4a7

23 files changed

+172
-79
lines changed

Cargo.lock

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ sha256 = "1.3.0"
1717
mutants = "0.0.3"
1818
sha2 = "0.10"
1919
serde = "1.0"
20-
visu = "0.2.3"
20+
visu = "0.2.4"
2121
crypto-hash = "0.3.4"
2222
itertools = "0.13"
2323
tokio = { version = "1.37", features = ["full"] }

contract/src/claim/api.rs

+11-3
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ impl Contract {
7171

7272
for jar in &unlocked_jars {
7373
let product = self.get_product(&jar.product_id);
74-
let (available_interest, remainder) = jar.get_interest(score, &product, now);
74+
75+
let (available_interest, remainder) = jar.get_interest(&score, &product, now);
7576

7677
let interest_to_claim = amount.map_or(available_interest, |amount| {
7778
cmp::min(available_interest, amount.0 - accumulator.get_total().0)
@@ -82,7 +83,8 @@ impl Contract {
8283

8384
jar.claim_remainder = remainder;
8485

85-
jar.claim(available_interest, interest_to_claim, now).lock();
86+
jar.claim(available_interest, interest_to_claim, &product, &score, now)
87+
.lock();
8688

8789
accumulator.add(jar.id, interest_to_claim);
8890

@@ -167,7 +169,13 @@ impl Contract {
167169

168170
jar.unlock();
169171

170-
if jar.should_be_closed(&product, now) {
172+
let score = self
173+
.account_score
174+
.get(&jar_before_transfer.account_id)
175+
.copied()
176+
.unwrap_or_default();
177+
178+
if jar.should_be_closed(&score, &product, now) {
171179
self.delete_jar(&jar_before_transfer.account_id, jar_before_transfer.id);
172180
}
173181
}

contract/src/claim/tests.rs

+8-8
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use near_sdk::{json_types::U128, test_utils::test_env::alice, PromiseOrValue};
44
use sweat_jar_model::{
55
api::{ClaimApi, JarApi, WithdrawApi},
66
claimed_amount_view::ClaimedAmountView,
7-
U32,
7+
AccountScore, U32,
88
};
99

1010
use crate::{
@@ -44,8 +44,8 @@ fn claim_total_detailed_when_having_tokens() {
4444
let product_term = product.get_lockup_term().unwrap();
4545
let test_duration = product_term + 100;
4646

47-
let jar_0_expected_interest = jar_0.get_interest(0, &product, test_duration).0;
48-
let jar_1_expected_interest = jar_1.get_interest(0, &product, test_duration).0;
47+
let jar_0_expected_interest = jar_0.get_interest(&AccountScore::default(), &product, test_duration).0;
48+
let jar_1_expected_interest = jar_1.get_interest(&AccountScore::default(), &product, test_duration).0;
4949

5050
context.set_block_timestamp_in_ms(test_duration);
5151

@@ -77,8 +77,8 @@ fn claim_partially_detailed_when_having_tokens() {
7777
let product_term = product.get_lockup_term().unwrap();
7878
let test_duration = product_term + 100;
7979

80-
let jar_0_expected_interest = jar_0.get_interest(0, &product, test_duration).0;
81-
let jar_1_expected_interest = jar_1.get_interest(0, &product, test_duration).0 - 1;
80+
let jar_0_expected_interest = jar_0.get_interest(&AccountScore::default(), &product, test_duration).0;
81+
let jar_1_expected_interest = jar_1.get_interest(&AccountScore::default(), &product, test_duration).0 - 1;
8282

8383
context.set_block_timestamp_in_ms(test_duration);
8484

@@ -115,8 +115,8 @@ fn claim_pending_withdraw_jar() {
115115
let product_term = product.get_lockup_term().unwrap();
116116
let test_duration = product_term + 100;
117117

118-
let jar_0_expected_interest = jar_0.get_interest(0, &product, test_duration);
119-
let jar_1_expected_interest = jar_1.get_interest(0, &product, test_duration).0 - 1;
118+
let jar_0_expected_interest = jar_0.get_interest(&AccountScore::default(), &product, test_duration);
119+
let jar_1_expected_interest = jar_1.get_interest(&AccountScore::default(), &product, test_duration).0 - 1;
120120

121121
context.set_block_timestamp_in_ms(test_duration);
122122

@@ -152,7 +152,7 @@ fn claim_partially_detailed_when_having_tokens_and_request_sum_of_single_deposit
152152
let product_term = product.get_lockup_term().unwrap();
153153
let test_duration = product_term + 100;
154154

155-
let jar_0_expected_interest = jar_0.get_interest(0, &product, test_duration).0;
155+
let jar_0_expected_interest = jar_0.get_interest(&AccountScore::default(), &product, test_duration).0;
156156

157157
context.set_block_timestamp_in_ms(test_duration);
158158

contract/src/jar/api.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ impl Contract {
4848

4949
let score = self.account_score.get(&account_id).copied().unwrap_or_default();
5050

51-
let withdraw_jar = jar.withdrawn(score, &product, principal, now);
52-
let should_be_closed = withdraw_jar.should_be_closed(&product, now);
51+
let withdraw_jar = jar.withdrawn(&score, &product, principal, now);
52+
let should_be_closed = withdraw_jar.should_be_closed(&score, &product, now);
5353

5454
if should_be_closed {
5555
self.delete_jar(&withdraw_jar.account_id, withdraw_jar.id);
@@ -132,7 +132,7 @@ impl JarApi for Contract {
132132
let score = self.account_score.get(&account_id).copied().unwrap_or_default();
133133

134134
for jar in self.account_jars_with_ids(&account_id, &jar_ids) {
135-
let interest = jar.get_interest(score, &self.get_product(&jar.product_id), now).0;
135+
let interest = jar.get_interest(&score, &self.get_product(&jar.product_id), now).0;
136136

137137
detailed_amounts.insert(U32(jar.id), U128(interest));
138138
total_amount += interest;

contract/src/jar/model/common.rs

+58-17
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@ use std::{cmp, cmp::min};
33
use ed25519_dalek::{Signature, VerifyingKey, PUBLIC_KEY_LENGTH, SIGNATURE_LENGTH};
44
use near_sdk::{
55
env,
6-
env::{panic_str, sha256},
6+
env::{block_timestamp_ms, panic_str, sha256},
77
json_types::{Base64VecU8, U128, U64},
88
near, require, AccountId,
99
};
1010
use sweat_jar_model::{
1111
jar::{JarId, JarView},
12-
ProductId, TokenAmount, MS_IN_YEAR,
12+
AccountScore, ProductId, TokenAmount, MS_IN_DAY, MS_IN_YEAR,
1313
};
1414

1515
use crate::{
@@ -50,8 +50,8 @@ impl JarLastVersion {
5050
self.is_pending_withdraw = false;
5151
}
5252

53-
pub(crate) fn apply_penalty(&mut self, product: &Product, is_applied: bool, now: Timestamp) {
54-
let current_interest = self.get_interest(0, product, now).0;
53+
pub(crate) fn apply_penalty(&mut self, score: &AccountScore, product: &Product, is_applied: bool, now: Timestamp) {
54+
let current_interest = self.get_interest(score, product, now).0;
5555

5656
self.cache = Some(JarCache {
5757
updated_at: now,
@@ -60,8 +60,14 @@ impl JarLastVersion {
6060
self.is_penalty_applied = is_applied;
6161
}
6262

63-
pub(crate) fn top_up(&mut self, amount: TokenAmount, product: &Product, now: Timestamp) -> &mut Self {
64-
let current_interest = self.get_interest(0, product, now).0;
63+
pub(crate) fn top_up(
64+
&mut self,
65+
score: &AccountScore,
66+
amount: TokenAmount,
67+
product: &Product,
68+
now: Timestamp,
69+
) -> &mut Self {
70+
let current_interest = self.get_interest(score, product, now).0;
6571

6672
self.principal += amount;
6773
self.cache = Some(JarCache {
@@ -75,18 +81,32 @@ impl JarLastVersion {
7581
&mut self,
7682
available_yield: TokenAmount,
7783
claimed_amount: TokenAmount,
84+
product: &Product,
85+
score: &AccountScore,
7886
now: Timestamp,
7987
) -> &mut Self {
8088
self.claimed_balance += claimed_amount;
89+
90+
let updated_at = if product.is_score_product() {
91+
let block_timestamp = block_timestamp_ms();
92+
if block_timestamp - score.last_update >= MS_IN_DAY {
93+
score.last_update + MS_IN_DAY
94+
} else {
95+
now
96+
}
97+
} else {
98+
now
99+
};
100+
81101
self.cache = Some(JarCache {
82-
updated_at: now,
102+
updated_at,
83103
interest: available_yield - claimed_amount,
84104
});
85105
self
86106
}
87107

88-
pub(crate) fn should_be_closed(&self, product: &Product, now: Timestamp) -> bool {
89-
!product.is_flexible() && self.principal == 0 && self.get_interest(0, product, now).0 == 0
108+
pub(crate) fn should_be_closed(&self, score: &AccountScore, product: &Product, now: Timestamp) -> bool {
109+
!product.is_flexible() && self.principal == 0 && self.get_interest(score, product, now).0 == 0
90110
}
91111

92112
/// Indicates whether a user can withdraw tokens from the jar at the moment or not.
@@ -138,10 +158,17 @@ impl JarLastVersion {
138158
)
139159
}
140160

141-
pub(crate) fn get_interest(&self, score: u32, product: &Product, now: Timestamp) -> (TokenAmount, u64) {
142-
let score = min(product.score_cap, score);
143-
let apy = self.get_apy(product) + Self::get_score_apy(score, product);
144-
self.get_interest_with_apy(apy, product, now)
161+
pub(crate) fn get_interest(
162+
&self,
163+
account_score: &AccountScore,
164+
product: &Product,
165+
now: Timestamp,
166+
) -> (TokenAmount, u64) {
167+
let (score_apy, adjusted_timestamp) = Self::get_score_apy(account_score, product);
168+
169+
let apy = self.get_apy(product) + score_apy;
170+
171+
self.get_interest_with_apy(apy, product, adjusted_timestamp.unwrap_or(now))
145172
}
146173

147174
pub(crate) fn get_apy(&self, product: &Product) -> UDecimal {
@@ -157,12 +184,24 @@ impl JarLastVersion {
157184
}
158185
}
159186

160-
pub(crate) fn get_score_apy(score: u32, product: &Product) -> UDecimal {
187+
pub(crate) fn get_score_apy(account_score: &AccountScore, product: &Product) -> (UDecimal, Option<Timestamp>) {
161188
if product.score_cap == 0 {
162-
return UDecimal::default();
189+
return (UDecimal::default(), None);
163190
}
164191

165-
UDecimal::new(score.into(), 5)
192+
let now = block_timestamp_ms();
193+
194+
let score = min(product.score_cap, account_score.score);
195+
196+
let apy = UDecimal::new(score.into(), 5);
197+
198+
// A 24 hours has passed since last steps update.
199+
// We need to wait for the next update form the server before updating score interest
200+
if now - account_score.last_update >= MS_IN_DAY {
201+
(apy, Some(account_score.last_update + MS_IN_DAY))
202+
} else {
203+
(apy, None)
204+
}
166205
}
167206

168207
fn get_interest_until_date(&self, product: &Product, now: Timestamp) -> Timestamp {
@@ -221,9 +260,11 @@ impl Contract {
221260

222261
let now = env::block_timestamp_ms();
223262

263+
let score = self.account_score.get(account).copied().unwrap_or_default();
264+
224265
let principal = self
225266
.get_jar_mut_internal(account, jar_id)
226-
.top_up(amount.0, &product, now)
267+
.top_up(&score, amount.0, &product, now)
227268
.principal;
228269

229270
emit(EventKind::TopUp(TopUpData { id: jar_id, amount }));

contract/src/jar/model/versioned.rs

+8-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use near_sdk::{
88
serde::{Deserialize, Serialize},
99
AccountId,
1010
};
11-
use sweat_jar_model::{jar::JarId, ProductId, Score, TokenAmount};
11+
use sweat_jar_model::{jar::JarId, AccountScore, ProductId, TokenAmount};
1212

1313
use crate::{
1414
common::Timestamp,
@@ -85,7 +85,13 @@ impl JarVersioned {
8585
self
8686
}
8787

88-
pub fn withdrawn(&self, score: Score, product: &Product, withdrawn_amount: TokenAmount, now: Timestamp) -> Self {
88+
pub fn withdrawn(
89+
&self,
90+
score: &AccountScore,
91+
product: &Product,
92+
withdrawn_amount: TokenAmount,
93+
now: Timestamp,
94+
) -> Self {
8995
JarV1 {
9096
principal: self.principal - withdrawn_amount,
9197
cache: Some(JarCache {

contract/src/jar/tests/tests.rs

+17-6
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
use fake::Fake;
44
use near_sdk::Timestamp;
5-
use sweat_jar_model::MS_IN_YEAR;
5+
use sweat_jar_model::{AccountScore, MS_IN_YEAR};
66

77
use crate::{
88
common::udecimal::UDecimal,
@@ -15,7 +15,7 @@ fn get_interest_before_maturity() {
1515
let product = Product::new().lockup_term(2 * MS_IN_YEAR);
1616
let jar = Jar::new(0).principal(100_000_000);
1717

18-
let interest = jar.get_interest(0, &product, MS_IN_YEAR).0;
18+
let interest = jar.get_interest(&AccountScore::default(), &product, MS_IN_YEAR).0;
1919
assert_eq!(12_000_000, interest);
2020
}
2121

@@ -24,7 +24,9 @@ fn get_interest_after_maturity() {
2424
let product = Product::new();
2525
let jar = Jar::new(0).principal(100_000_000);
2626

27-
let interest = jar.get_interest(0, &product, 400 * 24 * 60 * 60 * 1000).0;
27+
let interest = jar
28+
.get_interest(&AccountScore::default(), &product, 400 * 24 * 60 * 60 * 1000)
29+
.0;
2830
assert_eq!(12_000_000, interest);
2931
}
3032

@@ -33,12 +35,21 @@ fn interest_precision() {
3335
let product = Product::new().apy(Apy::Constant(UDecimal::new(1, 0)));
3436
let jar = Jar::new(0).principal(MS_IN_YEAR as u128);
3537

36-
assert_eq!(jar.get_interest(0, &product, 10000000000).0, 10000000000);
37-
assert_eq!(jar.get_interest(0, &product, 10000000001).0, 10000000001);
38+
assert_eq!(
39+
jar.get_interest(&AccountScore::default(), &product, 10000000000).0,
40+
10000000000
41+
);
42+
assert_eq!(
43+
jar.get_interest(&AccountScore::default(), &product, 10000000001).0,
44+
10000000001
45+
);
3846

3947
for _ in 0..100 {
4048
let time: Timestamp = (10..MS_IN_YEAR).fake();
41-
assert_eq!(jar.get_interest(0, &product, time).0, time as u128);
49+
assert_eq!(
50+
jar.get_interest(&AccountScore::default(), &product, time).0,
51+
time as u128
52+
);
4253
}
4354
}
4455

contract/src/lib.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use near_sdk::{
66
};
77
use near_self_update_proc::SelfUpdate;
88
use product::model::{Apy, Product};
9-
use sweat_jar_model::{api::InitApi, jar::JarId, ProductId, Score};
9+
use sweat_jar_model::{api::InitApi, jar::JarId, AccountScore, ProductId};
1010

1111
use crate::jar::model::{AccountJarsLegacy, Jar};
1212

@@ -55,7 +55,7 @@ pub struct Contract {
5555

5656
pub account_jars_v1: LookupMap<AccountId, AccountJarsLegacy>,
5757

58-
pub account_score: LookupMap<AccountId, Score>,
58+
pub account_score: LookupMap<AccountId, AccountScore>,
5959
}
6060

6161
#[near]

contract/src/penalty/api.rs

+6-2
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@ impl PenaltyApi for Contract {
2525

2626
assert_penalty_apy(&product.apy);
2727

28+
let score = self.account_score.get(&account_id).copied().unwrap_or_default();
29+
2830
self.get_jar_mut_internal(&account_id, jar_id)
29-
.apply_penalty(&product, value, now);
31+
.apply_penalty(&score, &product, value, now);
3032

3133
emit(ApplyPenalty(PenaltyData {
3234
id: jar_id,
@@ -50,6 +52,8 @@ impl PenaltyApi for Contract {
5052
.get_mut(&account_id)
5153
.unwrap_or_else(|| env::panic_str(&format!("Account '{account_id}' doesn't exist")));
5254

55+
let score = self.account_score.get(&account_id).copied().unwrap_or_default();
56+
5357
for jar_id in jars {
5458
let jar_id = jar_id.0;
5559

@@ -61,7 +65,7 @@ impl PenaltyApi for Contract {
6165
.unwrap_or_else(|| env::panic_str(&format!("Product '{}' doesn't exist", jar.product_id)));
6266

6367
assert_penalty_apy(&product.apy);
64-
jar.apply_penalty(&product, value, now);
68+
jar.apply_penalty(&score, &product, value, now);
6569

6670
applied_jars.push(jar_id);
6771
}

0 commit comments

Comments
 (0)