Skip to content

Commit f04f3f5

Browse files
committed
feat: claim-batch
1 parent cafb48a commit f04f3f5

File tree

8 files changed

+107
-70
lines changed

8 files changed

+107
-70
lines changed

contract/src/claim/api.rs

+32-24
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1+
use std::collections::HashMap;
2+
13
use near_sdk::{env, ext_contract, json_types::U128, near_bindgen, AccountId, PromiseOrValue};
24
use sweat_jar_model::{
3-
api::ClaimApi,
4-
claimed_amount_view::ClaimedAmountView,
5-
jar::{AggregatedTokenAmountView, JarIdView},
6-
U32,
5+
api::ClaimApi, claimed_amount_view::ClaimedAmountView, jar::AggregatedTokenAmountView, ProductId, TokenAmount,
76
};
87

98
use crate::{
@@ -12,7 +11,7 @@ use crate::{
1211
internal::is_promise_success,
1312
jar::model::Jar,
1413
score::AccountScore,
15-
Contract, ContractExt, JarsStorage,
14+
Contract, ContractExt, JarsStorage, Product,
1615
};
1716

1817
#[allow(dead_code)] // False positive since rust 1.78. It is used from `ext_contract` macro.
@@ -33,58 +32,67 @@ impl ClaimApi for Contract {
3332
fn claim_total(&mut self, detailed: Option<bool>) -> PromiseOrValue<ClaimedAmountView> {
3433
let account_id = env::predecessor_account_id();
3534
self.migrate_account_if_needed(&account_id);
36-
let jar_ids = self.account_jars(&account_id).iter().map(|a| U32(a.id)).collect();
37-
self.claim_jars_internal(account_id, jar_ids, detailed)
35+
self.claim_jars_internal(account_id, detailed)
3836
}
3937
}
4038

4139
impl Contract {
4240
fn claim_jars_internal(
4341
&mut self,
4442
account_id: AccountId,
45-
jar_ids: Vec<JarIdView>,
4643
detailed: Option<bool>,
4744
) -> PromiseOrValue<ClaimedAmountView> {
4845
let now = env::block_timestamp_ms();
4946
let mut accumulator = ClaimedAmountView::new(detailed);
5047

51-
let unlocked_jars: Vec<Jar> = self
52-
.account_jars(&account_id)
53-
.iter()
54-
.filter(|jar| !jar.is_pending_withdraw && jar_ids.contains(&U32(jar.id)))
55-
.cloned()
56-
.collect();
48+
let account_jars = self.account_jars(&account_id);
5749

58-
let mut event_data: Vec<ClaimEventItem> = vec![];
50+
// UnorderedMap doesn't have cache and deserializes `Product` on each get
51+
// This cache significantly reduces gas usage
52+
let mut products_cache: HashMap<ProductId, Product> = HashMap::new();
5953

6054
let account_score = self.get_score_mut(&account_id);
6155

6256
let account_score_before_transfer = account_score.as_ref().map(|s| **s);
6357

6458
let score = account_score.map(AccountScore::claim_score).unwrap_or_default();
6559

66-
for jar in &unlocked_jars {
67-
let product = self.get_product(&jar.product_id);
68-
let (interest, remainder) = jar.get_interest(&score, &product, now);
60+
let mut unlocked_jars: Vec<((TokenAmount, u64), &Jar)> = account_jars
61+
.iter()
62+
.filter(|jar| !jar.is_pending_withdraw)
63+
.map(|jar| {
64+
let product = products_cache
65+
.entry(jar.product_id.clone())
66+
.or_insert_with(|| self.get_product(&jar.product_id));
67+
(jar.get_interest(&score, product, now), jar)
68+
})
69+
.collect();
70+
71+
unlocked_jars.sort_by(|a, b| b.0 .0.cmp(&a.0 .0));
72+
73+
let jars_to_claim: Vec<_> = unlocked_jars.into_iter().take(100).collect();
74+
75+
let mut event_data: Vec<ClaimEventItem> = vec![];
6976

70-
if interest > 0 {
77+
for ((available_interest, remainder), jar) in &jars_to_claim {
78+
if *available_interest > 0 {
7179
let jar = self.get_jar_mut_internal(&jar.account_id, jar.id);
7280

73-
jar.claim_remainder = remainder;
81+
jar.claim_remainder = *remainder;
7482

75-
jar.claim(interest, now).lock();
83+
jar.claim(*available_interest, now).lock();
7684

77-
accumulator.add(jar.id, interest);
85+
accumulator.add(jar.id, *available_interest);
7886

79-
event_data.push((jar.id, U128(interest)));
87+
event_data.push((jar.id, U128(*available_interest)));
8088
}
8189
}
8290

8391
if accumulator.get_total().0 > 0 {
8492
self.claim_interest(
8593
&account_id,
8694
accumulator,
87-
unlocked_jars,
95+
jars_to_claim.into_iter().map(|a| a.1).cloned().collect(),
8896
account_score_before_transfer,
8997
EventKind::Claim(event_data),
9098
now,
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#![cfg(feature = "integration-test")]
22

33
use near_sdk::{env, near_bindgen, AccountId, Timestamp};
4-
use sweat_jar_model::{api::IntegrationTestMethods, jar::JarView, ProductId};
4+
use sweat_jar_model::{api::IntegrationTestMethods, ProductId};
55

66
use crate::{jar::model::Jar, Contract, ContractExt};
77

@@ -12,17 +12,11 @@ impl IntegrationTestMethods for Contract {
1212
env::block_timestamp_ms()
1313
}
1414

15-
fn bulk_create_jars(
16-
&mut self,
17-
account_id: AccountId,
18-
product_id: ProductId,
19-
principal: u128,
20-
number_of_jars: u16,
21-
) -> Vec<JarView> {
15+
fn bulk_create_jars(&mut self, account_id: AccountId, product_id: ProductId, principal: u128, number_of_jars: u16) {
2216
self.assert_manager();
17+
let now = env::block_timestamp_ms();
2318
(0..number_of_jars)
24-
.map(|_| self.create_jar_for_integration_tests(&account_id, &product_id, principal))
25-
.collect()
19+
.for_each(|_| self.create_jar_for_integration_tests(&account_id, &product_id, principal, now));
2620
}
2721
}
2822

@@ -33,18 +27,11 @@ impl Contract {
3327
account_id: &AccountId,
3428
product_id: &ProductId,
3529
amount: u128,
36-
) -> JarView {
37-
let product = self.get_product(&product_id);
38-
39-
product.assert_enabled();
40-
product.assert_cap(amount);
41-
30+
now: u64,
31+
) {
4232
let id = self.increment_and_get_last_jar_id();
43-
let now = env::block_timestamp_ms();
4433
let jar = Jar::create(id, account_id.clone(), product_id.clone(), amount, now);
4534

46-
self.add_new_jar(account_id, jar.clone());
47-
48-
jar.into()
35+
self.add_new_jar(account_id, jar);
4936
}
5037
}

contract/src/jar/api.rs

+11-2
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ use near_sdk::{env, env::panic_str, json_types::U128, near_bindgen, require, Acc
44
use sweat_jar_model::{
55
api::JarApi,
66
jar::{AggregatedInterestView, AggregatedTokenAmountView, JarId, JarIdView, JarView},
7-
TokenAmount, U32,
7+
ProductId, TokenAmount, U32,
88
};
99

1010
use crate::{
1111
event::{emit, EventKind, RestakeData},
1212
jar::model::Jar,
13+
product::model::Product,
1314
score::AccountScore,
1415
Contract, ContractExt, JarsStorage,
1516
};
@@ -145,13 +146,21 @@ impl JarApi for Contract {
145146
let mut detailed_amounts = HashMap::<JarIdView, U128>::new();
146147
let mut total_amount: TokenAmount = 0;
147148

149+
// UnorderedMap doesn't have cache and deserializes `Product` on each get
150+
// This cache significantly reduces gas usage
151+
let mut products_cache: HashMap<ProductId, Product> = HashMap::new();
152+
148153
let score = self
149154
.get_score(&account_id)
150155
.map(AccountScore::claimable_score)
151156
.unwrap_or_default();
152157

153158
for jar in self.account_jars_with_ids(&account_id, &jar_ids) {
154-
let interest = jar.get_interest(&score, &self.get_product(&jar.product_id), now).0;
159+
let product = products_cache
160+
.entry(jar.product_id.clone())
161+
.or_insert_with(|| self.get_product(&jar.product_id));
162+
163+
let interest = jar.get_interest(&score, product, now).0;
155164

156165
detailed_amounts.insert(U32(jar.id), U128(interest));
157166
total_amount += interest;

integration-tests/src/context.rs

+5-3
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ pub trait ContextHelpers {
138138
product_id: &ProductId,
139139
principal: u128,
140140
number_of_jars: u16,
141-
) -> Result<Vec<JarView>>;
141+
) -> Result<()>;
142142
async fn account_balance(&self, account: &Account) -> Result<u128>;
143143
}
144144

@@ -159,7 +159,7 @@ impl ContextHelpers for Context {
159159
product_id: &ProductId,
160160
principal: u128,
161161
number_of_jars: u16,
162-
) -> Result<Vec<JarView>> {
162+
) -> Result<()> {
163163
let total_amount = principal * number_of_jars as u128;
164164

165165
self.ft_contract()
@@ -189,7 +189,9 @@ impl ContextHelpers for Context {
189189
self.sweat_jar()
190190
.bulk_create_jars(account.to_near(), product_id.clone(), principal, number_of_jars)
191191
.with_user(&manager)
192-
.await
192+
.await?;
193+
194+
Ok(())
193195
}
194196

195197
async fn account_balance(&self, account: &Account) -> Result<u128> {

integration-tests/src/many_jars.rs

+48-14
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use anyhow::Result;
2-
use nitka::misc::ToNear;
2+
use nitka::{misc::ToNear, set_integration_logs_enabled};
33
use sweat_jar_model::api::{ClaimApiIntegration, IntegrationTestMethodsIntegration, JarApiIntegration};
44

55
use crate::{
@@ -12,32 +12,66 @@ use crate::{
1212
async fn claim_many_jars() -> Result<()> {
1313
println!("👷🏽 Claim many jars test");
1414

15+
set_integration_logs_enabled(false);
16+
1517
let mut context = prepare_contract(None, [Locked5Minutes60000Percents]).await?;
1618

1719
let alice = context.alice().await?;
1820
let manager = context.manager().await?;
1921

2022
context
2123
.sweat_jar()
22-
.bulk_create_jars(alice.to_near(), Locked5Minutes60000Percents.id(), 10000, 450)
24+
.bulk_create_jars(alice.to_near(), Locked5Minutes60000Percents.id(), 1000, 4000)
2325
.with_user(&manager)
2426
.await?;
2527

28+
dbg!(context.sweat_jar().get_jars_for_account(alice.to_near()).await?.len());
29+
2630
context.fast_forward_minutes(5).await?;
2731

28-
context
29-
.sweat_jar()
30-
.claim_total(true.into())
31-
.with_user(&alice)
32-
.result()
33-
.await?;
32+
let claimed = context.sweat_jar().claim_total(true.into()).with_user(&alice).await?;
3433

35-
assert!(context
36-
.sweat_jar()
37-
.get_jars_for_account(alice.to_near())
38-
.await?
39-
.iter()
40-
.all(|j| j.is_pending_withdraw == false));
34+
let batch_claim_summ = claimed.get_total().0;
35+
36+
dbg!(&batch_claim_summ);
37+
38+
assert_eq!(
39+
batch_claim_summ * 39,
40+
context
41+
.sweat_jar()
42+
.get_total_interest(alice.to_near())
43+
.await?
44+
.amount
45+
.total
46+
.0
47+
);
48+
49+
for i in 1..40 {
50+
let claimed = context.sweat_jar().claim_total(true.into()).with_user(&alice).await?;
51+
assert_eq!(claimed.get_total().0, batch_claim_summ);
52+
53+
assert_eq!(
54+
batch_claim_summ * (39 - i),
55+
context
56+
.sweat_jar()
57+
.get_total_interest(alice.to_near())
58+
.await?
59+
.amount
60+
.total
61+
.0
62+
);
63+
}
64+
65+
assert_eq!(
66+
context
67+
.sweat_jar()
68+
.get_total_interest(alice.to_near())
69+
.await?
70+
.amount
71+
.total
72+
.0,
73+
0
74+
);
4175

4276
Ok(())
4377
}

integration-tests/src/withdraw_all.rs

+3
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ async fn withdraw_all() -> Result<()> {
5959

6060
context.fast_forward_minutes(6).await?;
6161

62+
// 3 calls to claim all 210 jars
63+
context.sweat_jar().claim_total(None).with_user(&alice).await?;
64+
context.sweat_jar().claim_total(None).with_user(&alice).await?;
6265
context.sweat_jar().claim_total(None).with_user(&alice).await?;
6366

6467
let alice_balance = context.ft_contract().ft_balance_of(alice.to_near()).await?;

model/src/api.rs

+1-7
Original file line numberDiff line numberDiff line change
@@ -295,11 +295,5 @@ pub trait ScoreApi {
295295
#[make_integration_version]
296296
pub trait IntegrationTestMethods {
297297
fn block_timestamp_ms(&self) -> near_sdk::Timestamp;
298-
fn bulk_create_jars(
299-
&mut self,
300-
account_id: AccountId,
301-
product_id: ProductId,
302-
principal: u128,
303-
number_of_jars: u16,
304-
) -> Vec<JarView>;
298+
fn bulk_create_jars(&mut self, account_id: AccountId, product_id: ProductId, principal: u128, number_of_jars: u16);
305299
}

res/sweat_jar.wasm

9.67 KB
Binary file not shown.

0 commit comments

Comments
 (0)