Skip to content
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

feat: battlepass #120

Merged
merged 4 commits into from
Dec 16, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
47 changes: 47 additions & 0 deletions battlepass/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
# ███░▄▄▄█░▄▄▀█░▄▀▄░█░▄▄█░▄▀█░▄▄▀█▀▄▄▀██
# ███░█▄▀█░▀▀░█░█▄█░█░▄▄█░█░█░▀▀░█░██░██
# ███▄▄▄▄█▄██▄█▄███▄█▄▄▄█▄▄██▄██▄██▄▄███
# ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
[package]
name = "gamedao-battlepass"
version = "1.2.0"
authors = ["zero.io","gamedao.co"]
repository = "https://github.com/gamedaoco/gamedao-protocol"
edition = "2018"
license = "GPL-3.0-or-later"
description = "BattlePass pallet provides functionality to create, manage and participate in battlepasses."

[dependencies]
serde = { version = "1.0.143", optional = true }
codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive"] }
scale-info = { version = "2.1.1", default-features = false, features = ["derive"] }
sp-core = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.28" }
sp-runtime = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.28" }
sp-std = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.28" }
sp-storage = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.28" }
sp-io = { git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.28", default-features=false }
frame-support = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.28", default-features = false }
frame-system = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.28", default-features = false }
frame-benchmarking = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.28", default-features = false, optional = true }

[dev-dependencies]
frame-support-test = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.28" }

[features]
default = ['std']
runtime-benchmarks = ["frame-benchmarking"]
std = [
"serde/std",
'codec/std',
"scale-info/std",

"frame-support/std",
"frame-system/std",
"frame-benchmarking/std",

"sp-core/std",
"sp-std/std",
"sp-runtime/std",
]
try-runtime = ["frame-support/try-runtime"]
185 changes: 185 additions & 0 deletions battlepass/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
// _______ ________ ________ ________ ______ _______ _______
// ╱╱ ╲╱ ╲╱ ╲╱ ╲_╱ ╲╲╱ ╲╲╱ ╲╲
// ╱╱ __╱ ╱ ╱ ╱ ╱╱ ╱╱ ╱╱
// ╱ ╱ ╱ ╱ ╱ _╱ ╱ ╱ ╱
// ╲________╱╲___╱____╱╲__╱__╱__╱╲________╱╲________╱╲___╱____╱╲________╱
//
// This file is part of GameDAO Protocol.
// Copyright (C) 2018-2022 GameDAO AG.
// SPDX-License-Identifier: Apache-2.0

//! BATTLEPASS
//! This pallet provides functionality to create, manage and participate in battlepasses.
#![cfg_attr(not(feature = "std"), no_std)]

pub use pallet::*;
use frame_support::pallet_prelude::*;
use frame_system::pallet_prelude::*;
use sp_std::convert::TryInto;
use sp_runtime::traits::Hash;

pub mod types;
pub use types::*;

type String<T> = BoundedVec<u8, <T as pallet::Config>::StringLimit>;

#[frame_support::pallet]
pub mod pallet {
use super::*;

#[pallet::pallet]
#[pallet::generate_store(pub(super) trait Store)]
pub struct Pallet<T>(_);

#[pallet::config]
pub trait Config: frame_system::Config {
type Event: From<Event<Self>>
+ IsType<<Self as frame_system::Config>::Event>
+ Into<<Self as frame_system::Config>::Event>;

/// The maximum length of a name or cid stored on-chain.
#[pallet::constant]
type StringLimit: Get<u32>;
}

#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
/// New BattlePass created
BattlepassCreated {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest using "Created", "Claimed", "Closed" instead. Like we have for Flow. Since the subject is always the same - Battlepass.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There will be other entities in the pallet, like Quests, Achievements, etc which will have similar events.
Of course, we may put them in separate pallets, but they are related only to Battlepass, so I'm not sure if we should do so.
WDYT @vovacha , @vayesy , @2075 ?

// org_id: T::Hash,
org_id: T::Hash,
battlepass_id: T::Hash,
block_number: T::BlockNumber
},

/// BattlePass claimed
BattlepassClaimed {
claimer: T::AccountId,
org_id: T::Hash,
battlepass_id: T::Hash,
nft_id: T::Hash,
block_number: T::BlockNumber
FiberMan marked this conversation as resolved.
Show resolved Hide resolved
},
}

#[pallet::error]
pub enum Error<T> {
AuthorizationError,
MissingParameter,
InvalidParameter,
OrganizationUnknown,
OrgHasActiveBattlepass,
BattlepassExists,
BattlepassClaimed,
BattlepassUnknown,
NotMember,
}

/// Battlepass by its id.
///
/// Battlepasses: map Hash => Battlepass
#[pallet::storage]
#[pallet::getter(fn get_battlepass)]
pub(super) type Battlepasses<T: Config> = StorageMap<_, Blake2_128Concat, T::Hash, Battlepass<T::Hash, T::AccountId, T::BlockNumber, String<T>>, OptionQuery>;
FiberMan marked this conversation as resolved.
Show resolved Hide resolved

/// Number of battlepasses per organization.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it the number of claimed passes ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it's the total number of created battlepasses per org.

Copy link
Member

@2075 2075 Dec 16, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so created not claimed? what is the difference? i think the pass is minted when you claim, so claimed==created.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, it's a different entity, as @vovacha explained here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you mean what @vayesy wrote? or do you reference something else?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry, yes, I meant @vayesy.

///
/// BattlepassCount: map Hash => u32
#[pallet::storage]
#[pallet::getter(fn get_battlepass_count)]
pub type BattlepassCount<T: Config> = StorageMap<_, Blake2_128Concat, T::Hash, u32, ValueQuery>;

/// Current active battlepass in organization.
///
/// ActiveBattlepassByOrg: map Hash => Hash
#[pallet::storage]
#[pallet::getter(fn get_active_battlepass)]
pub type ActiveBattlepassByOrg<T: Config> = StorageMap<_, Blake2_128Concat, T::Hash, T::Hash, OptionQuery>;
FiberMan marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

@vovacha vovacha Dec 16, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is only one usage of this storage and it's avoiding duplicates. And it's possible to achieve this with the existing Battlepass storage if you do the next steps:

  • remove created and updated from the Battlepass struct, since indexer nows this information it's redundant here
  • this way you'll have a unique hash of the Battlepass without any variables like BlockNumber
  • check if Battlepass doesn't contain a hash

Copy link
Contributor Author

@FiberMan FiberMan Dec 19, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea. But in this case, we need to keep in mind to recreate all Battlepasses if something changed in Battlepass struct.
And with this approach, we can only check if the same Battlepass exists, but we need to check if there is an active battlepass.


/// Claimed Battlepass by user.
///
/// ClaimedBattlepass: map (AccountId, Hash) => Hash
#[pallet::storage]
#[pallet::getter(fn get_claimed_battlepass)]
pub(super) type ClaimedBattlepass<T: Config> = StorageDoubleMap<_,
Blake2_128Concat, T::AccountId,
Blake2_128Concat, T::Hash,
T::Hash,
OptionQuery
>;

#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::weight(0)]
pub fn create_battlepass(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you put the transactional part into the extrinsic fn, you may need to double the code unneccessarily as we need a way to add a battlepass via oracle, therefore who, pass, account where who is permitted oracle and account is the user to add. also we need to store respective discord id somewhere otherwise we cannot map it,.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there is slight confusion in terminology.
We name two entities the same way: a) general entity, which is created by organization and represents event to participate for the users; b) entity, which users purchase to gain access to participate in the event.
Particularly this extrinsic is used to create a) entity type. So we don't need to store discord id here and extend functionality for the oracle. Those will be applied for working with b) entity type.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

battlepass is the general seasonal wrapper for achievements which result in scores and which unlock claiming rewards. i have written a doc for it

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you add the proper names for those entities and structs and show where they are stored? @vayesy

origin: OriginFor<T>,
org_id: T::Hash,
name: String<T>,
cid: String<T>,
price: u16,
) -> DispatchResult {
let creator = ensure_signed(origin)?;

// check if origin is Org Prime or Root

// check if Org exists

// check if active battlepass does not exist for the Org



// Create Battlepass
let now = <frame_system::Pallet<T>>::block_number();
let battlepass: Battlepass<T::Hash, T::AccountId, T::BlockNumber, String<T>> = types::Battlepass {
creator,
org_id,
name,
cid,
state: types::BattlepassState::Active,
season: Self::get_battlepass_count(org_id),
price,
FiberMan marked this conversation as resolved.
Show resolved Hide resolved
created: now.clone(), mutated: now
};
let battlepass_id = <T as frame_system::Config>::Hashing::hash_of(&battlepass);

Battlepasses::<T>::insert(&battlepass_id, battlepass);

Self::deposit_event(Event::BattlepassCreated { org_id, battlepass_id, block_number: now });

Ok(())
}

#[pallet::weight(0)]
pub fn claim_battlepass(
origin: OriginFor<T>,
org_id: T::Hash,
) -> DispatchResult {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why use org_id here instead of battlepass_id.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would work though

  • claim the active battlepass for an org would require this
  • or param is optional org or battlepass id
  • agree it is cleaner to use battlepass id

let claimer = ensure_signed(origin)?;
let battlepass_id = Self::get_active_battlepass(org_id).ok_or(Error::<T>::BattlepassUnknown)?;
let now = <frame_system::Pallet<T>>::block_number();

// check if user is a member of organization

// check if Org exists
2075 marked this conversation as resolved.
Show resolved Hide resolved

// check if Org has active battlepass
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you look to the two checks above maybe we should introduce the same for battlepass, like T::Battlepass::is_battlepass_active ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we need to get the active battlepass, not just check if it's active.


// check if Battlepass already claimed
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you'd use battlepass_id as an extrinsic param, you can remove this line.





// Create NFT
let nft_id = <T as frame_system::Config>::Hashing::hash_of(&claimer);


ClaimedBattlepass::<T>::insert(&claimer, battlepass_id, nft_id);

Self::deposit_event(Event::BattlepassClaimed { claimer, org_id, battlepass_id, nft_id, block_number: now });

Ok(())
}

}
}
29 changes: 29 additions & 0 deletions battlepass/src/types.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
use frame_support::pallet_prelude::*;
use codec::MaxEncodedLen;

#[derive(Encode, Decode, Clone, PartialEq, Eq, PartialOrd, Ord, TypeInfo, MaxEncodedLen)]
// #[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))]
pub enum BattlepassState {
Inactive = 0,
Active = 1,
}
impl Default for BattlepassState {
fn default() -> Self {
Self::Active
}
}


#[derive(Encode, Decode, Default, PartialEq, Eq, TypeInfo, MaxEncodedLen)]
// #[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug))]
pub struct Battlepass<Hash, AccountId, BlockNumber, BoundedString> {
pub creator: AccountId,
pub org_id: Hash,
pub name: BoundedString,
pub cid: BoundedString,
pub state: BattlepassState,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usually, we have separate storage for the state (ex. MemberStates, OrgStates). I'm not sure what ActiveBattlepassByOrg is, but storing the state in the struct and storing it in the separate storage is suboptimal since you need to update both.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will create storage for states, like battlepass_id -> BattlepassState
ActiveBattlepassByOrg - is to check which is a currently active battlepass for the Org (org_id -> active battlepass_id). So the Active state will be stored in two places, but it will bring more benefits in terms of storage and usability, I suppose.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can query the active battlepass either via the graph as a client or by filtering for active pass as a first class citizen (the pallet), what is the usecase for the pallet? it would only operate per id on a specific battlepass and reject if the operation is not allowed, e.g. not active, not owner

e.g. org -> vec<battlepass>

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Example of use case: upon creation of new Battlepass for the Org, we need to check if there is an Active Battlepass (current season) already. Org may have multiple Battlepasses, but only one can be Active. So having a mapping org -> active battlepass is more convenient and faster than working with vec.

pub season: u32,
pub price: u16,
pub created: BlockNumber,
pub mutated: BlockNumber,
FiberMan marked this conversation as resolved.
Show resolved Hide resolved
}
4 changes: 2 additions & 2 deletions flow/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,7 @@ use frame_support::{

use scale_info::TypeInfo;
use sp_runtime::{traits::{AtLeast32BitUnsigned, Hash}, Permill, ArithmeticError::Overflow};

use sp_std::{vec, vec::Vec, convert::{TryFrom, TryInto}};
use sp_std::{vec::Vec, convert::{TryFrom, TryInto}};

use gamedao_traits::{ControlTrait, ControlBenchmarkingTrait, FlowTrait, FlowBenchmarkingTrait};
use orml_traits::{MultiCurrency, MultiReservableCurrency};
Expand Down Expand Up @@ -622,6 +621,7 @@ impl<T: Config> FlowBenchmarkingTrait<T::AccountId, T::BlockNumber, T::Hash> for
#[cfg(feature = "runtime-benchmarks")]
fn create_campaign(caller: &T::AccountId, org_id: &T::Hash, start: T::BlockNumber) -> Result<T::Hash, &'static str> {
use sp_runtime::traits::Saturating;
use sp_std::vec;
let bounded_str: BoundedVec<u8, T::StringLimit> = BoundedVec::truncate_from(vec![0; T::StringLimit::get() as usize]);
let now = frame_system::Pallet::<T>::block_number();
let index = CampaignCount::<T>::get();
Expand Down