Skip to content

on Chain Claim Seasons #147

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 63 additions & 8 deletions contracts/on-chain-claim/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
use multiversx_sc::imports::*;

use crate::address_info::*;
use multiversx_sc::imports::*;

pub const MAX_REPAIR_GAP: u64 = 5;
pub const FIRST_SEASON_ID: usize = 1;
pub const FIRST_SEASON_START_EPOCH: u64 = 1400;

type Epoch = u64;

#[multiversx_sc::module]
pub trait ConfigModule {
Expand All @@ -24,32 +27,84 @@ pub trait ConfigModule {

#[view(getAddressInfo)]
fn get_address_info(&self, address: &ManagedAddress) -> AddressInfo {
let current_season_id: usize = self.get_current_season();
let address_info_by_season_mapper = self.address_info_by_season(address, current_season_id);

if !address_info_by_season_mapper.is_empty() {
return address_info_by_season_mapper.get();
}

let address_info_mapper = self.address_info(address);
if current_season_id == FIRST_SEASON_ID && !address_info_mapper.is_empty() {
return address_info_mapper.get();
}

AddressInfo::default()
}

#[view(getAddressInfoBySeason)]
fn get_address_info_by_season(
&self,
address: &ManagedAddress,
season_id: usize,
) -> AddressInfo {
let address_info_by_season_mapper = self.address_info_by_season(address, season_id);

if address_info_mapper.is_empty() {
return AddressInfo::default();
if !address_info_by_season_mapper.is_empty() {
return address_info_by_season_mapper.get();
}

address_info_mapper.get()
let address_info_mapper = self.address_info(address);
if season_id == FIRST_SEASON_ID && !address_info_mapper.is_empty() {
return address_info_mapper.get();
}

AddressInfo::default()
}

#[view(canBeRepaired)]
fn can_be_repaired(&self, address: &ManagedAddress) -> bool {
let address_info_mapper = self.address_info(address);
if address_info_mapper.is_empty() {
let address_info = self.get_address_info(address);
if address_info.total_epochs_claimed == 0 {
return false;
}

let address_info = address_info_mapper.get();
let current_epoch = self.blockchain().get_block_epoch();
let missed_epochs = self.get_missed_epochs(current_epoch, address_info.last_epoch_claimed);

missed_epochs > 0 && missed_epochs <= MAX_REPAIR_GAP
}

#[view(getCurrentSeason)]
fn get_current_season(&self) -> usize {
let current_epoch = self.blockchain().get_block_epoch();
let seasons = self.seasons();

let last_season_starting_epoch = seasons.get(seasons.len());
if last_season_starting_epoch <= current_epoch {
return seasons.len();
}

seasons.len() - 1
}

#[storage_mapper("address_info")]
fn address_info(&self, address: &ManagedAddress) -> SingleValueMapper<AddressInfo>;

#[storage_mapper("address_info_by_season")]
fn address_info_by_season(
&self,
address: &ManagedAddress,
season_id: usize,
) -> SingleValueMapper<AddressInfo>;

/**
* This parameter is referring to the start epoch of the season.
*/
#[view(getSeasons)]
#[storage_mapper("seasons")]
fn seasons(&self) -> VecMapper<Epoch>;

#[view(getRepairStreakPayment)]
#[storage_mapper("repair_streak_payment")]
fn repair_streak_payment(&self) -> SingleValueMapper<EsdtTokenPayment>;
Expand Down
165 changes: 118 additions & 47 deletions contracts/on-chain-claim/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
#![allow(unused_attributes)]

pub use address_info::AddressInfo;
use config::{FIRST_SEASON_ID, FIRST_SEASON_START_EPOCH};

use multiversx_sc::imports::*;

pub mod address_info;
Expand All @@ -21,10 +23,26 @@ pub trait OnChainClaimContract:

let caller = self.blockchain().get_caller();
self.add_admin(caller);

self.seasons().push(&FIRST_SEASON_START_EPOCH);
}

#[upgrade]
fn upgrade(&self) {}
fn upgrade(&self) {
if self.seasons().is_empty() {
self.seasons().push(&FIRST_SEASON_START_EPOCH);
}
}

fn migrate_legacy_address_info(&self, address: &ManagedAddress) {
if self.address_info(&address).is_empty() {
return;
}

let address_info = self.address_info(&address).take();
self.address_info_by_season(address, FIRST_SEASON_ID)
.set(address_info);
}

#[endpoint(claim)]
fn claim(&self) {
Expand All @@ -35,36 +53,22 @@ pub trait OnChainClaimContract:
);
self.require_same_shard(&caller);

self.migrate_legacy_address_info(&caller);

let current_epoch = self.blockchain().get_block_epoch();
let current_season_id = self.get_current_season();

let address_info_mapper = self.address_info(&caller);
if address_info_mapper.is_empty() {
let address_info_by_season_mapper = self.address_info_by_season(&caller, current_season_id);
if address_info_by_season_mapper.is_empty() {
let address_info = AddressInfo::new_with_epoch(current_epoch);
self.address_info(&caller).set(&address_info);
self.address_info_by_season(&caller, current_season_id)
.set(&address_info);
self.new_claim_event(&caller, &address_info);
return;
}

address_info_mapper.update(|address_info| {
require!(
address_info.last_epoch_claimed < current_epoch,
"epoch already claimed"
);

if address_info.last_epoch_claimed + 1 == current_epoch {
address_info.current_streak += 1;
} else {
address_info.current_streak = 1;
}

address_info.total_epochs_claimed += 1;
address_info.last_epoch_claimed = current_epoch;

if address_info.best_streak < address_info.current_streak {
address_info.best_streak = address_info.current_streak;
}

self.new_claim_event(&caller, address_info);
address_info_by_season_mapper.update(|address_info| {
self.increment_address_info(address_info, current_epoch);
});
}

Expand All @@ -78,38 +82,24 @@ pub trait OnChainClaimContract:
);
self.require_same_shard(&caller);

self.migrate_legacy_address_info(&caller);

let payment = self.call_value().single_esdt();
let repair_streak_payment = self.repair_streak_payment().get();
require!(payment == repair_streak_payment, "Bad payment token/amount");

let current_epoch = self.blockchain().get_block_epoch();

let address_info_mapper = self.address_info(&caller);
let current_season_id = self.get_current_season();
let address_info = self.get_address_info(&caller);

require!(
!address_info_mapper.is_empty(),
address_info.total_epochs_claimed > 0,
"can't repair streak for address"
);

address_info_mapper.update(|address_info| {
let missed_epochs =
self.get_missed_epochs(current_epoch, address_info.last_epoch_claimed);

// Allow MAX_REPAIR_GAP + 1 in order to not have failed transaction when the user sends the claimAndRepair transaction
// in the last round of the allowed epoch. From UI, we allow MAX_REPAIR_GAP = 5 (using canBeRepaired view)
require!(
missed_epochs > 0 && missed_epochs <= MAX_REPAIR_GAP + 1,
"can't repair streak for current epoch"
);

address_info.current_streak += missed_epochs + 1;
address_info.total_epochs_claimed += missed_epochs + 1;
address_info.last_epoch_claimed = current_epoch;
if address_info.best_streak < address_info.current_streak {
address_info.best_streak = address_info.current_streak;
}

self.new_claim_and_repair_event(&caller, address_info);
let address_info_by_season_mapper = self.address_info_by_season(&caller, current_season_id);
address_info_by_season_mapper.update(|address_info| {
self.repair_address_info_streak(&caller, address_info, current_epoch);
});

self.send().esdt_local_burn(
Expand All @@ -119,9 +109,36 @@ pub trait OnChainClaimContract:
);
}

#[endpoint(addSeason)]
fn add_season(&self, epoch: u64) {
self.require_caller_is_admin();

let mut seasons = self.seasons();

if seasons.is_empty() {
seasons.push(&epoch);
return;
}

let current_epoch = self.blockchain().get_block_epoch();
let last_season_starting_epoch = seasons.get(seasons.len());

require!(
last_season_starting_epoch < current_epoch,
"last season must start before the current epoch"
);

require!(
current_epoch < epoch,
"new season must start after the last season"
);
seasons.push(&epoch);
}

#[endpoint(updateState)]
fn update_state(
&self,
season_id: usize,
address: &ManagedAddress,
current_streak: u64,
last_epoch_claimed: u64,
Expand All @@ -130,14 +147,23 @@ pub trait OnChainClaimContract:
) {
self.require_caller_is_admin();
self.require_same_shard(address);
let current_season_id = self.get_current_season();
require!(
current_season_id == season_id,
"season must be the current season"
);

self.migrate_legacy_address_info(address);

let address_info = AddressInfo::new(
current_streak,
last_epoch_claimed,
total_epochs_claimed,
best_streak,
);
self.address_info(address).set(&address_info);

self.address_info_by_season(address, current_season_id)
.set(&address_info);

self.new_update_state_event(address, &address_info);
}
Expand Down Expand Up @@ -173,4 +199,49 @@ pub trait OnChainClaimContract:
);
self.repair_streak_payment().set(payment);
}

fn increment_address_info(&self, address_info: &mut AddressInfo, current_epoch: u64) {
require!(
address_info.last_epoch_claimed < current_epoch,
"epoch already claimed"
);

if address_info.last_epoch_claimed + 1 == current_epoch {
address_info.current_streak += 1;
} else {
address_info.current_streak = 1;
}

address_info.total_epochs_claimed += 1;
address_info.last_epoch_claimed = current_epoch;

if address_info.best_streak < address_info.current_streak {
address_info.best_streak = address_info.current_streak;
}
}

fn repair_address_info_streak(
&self,
caller: &ManagedAddress,
address_info: &mut AddressInfo,
current_epoch: u64,
) {
let missed_epochs = self.get_missed_epochs(current_epoch, address_info.last_epoch_claimed);

// Allow MAX_REPAIR_GAP + 1 in order to not have failed transaction when the user sends the claimAndRepair transaction
// in the last round of the allowed epoch. From UI, we allow MAX_REPAIR_GAP = 5 (using canBeRepaired view)
require!(
missed_epochs > 0 && missed_epochs <= MAX_REPAIR_GAP + 1,
"can't repair streak for current epoch"
);

address_info.current_streak += missed_epochs + 1;
address_info.total_epochs_claimed += missed_epochs + 1;
address_info.last_epoch_claimed = current_epoch;
if address_info.best_streak < address_info.current_streak {
address_info.best_streak = address_info.current_streak;
}

self.new_claim_and_repair_event(&caller, address_info);
}
}
Loading