feat: Rebrand Polkadot/Substrate references to PezkuwiChain
This commit systematically rebrands various references from Parity Technologies' Polkadot/Substrate ecosystem to PezkuwiChain within the kurdistan-sdk. Key changes include: - Updated external repository URLs (zombienet-sdk, parity-db, parity-scale-codec, wasm-instrument) to point to pezkuwichain forks. - Modified internal documentation and code comments to reflect PezkuwiChain naming and structure. - Replaced direct references to with or specific paths within the for XCM, Pezkuwi, and other modules. - Cleaned up deprecated issue and PR references in various and files, particularly in and modules. - Adjusted image and logo URLs in documentation to point to PezkuwiChain assets. - Removed or rephrased comments related to external Polkadot/Substrate PRs and issues. This is a significant step towards fully customizing the SDK for the PezkuwiChain ecosystem.
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
[package]
|
||||
name = "pezpallet-collective-content"
|
||||
version = "0.6.0"
|
||||
authors = [
|
||||
"Kurdistan Tech Institute <info@pezkuwichain.io>",
|
||||
"Parity Technologies <admin@parity.io>",
|
||||
]
|
||||
edition.workspace = true
|
||||
description = "Managed content"
|
||||
license = "Apache-2.0"
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
codec = { features = ["derive", "max-encoded-len"], workspace = true }
|
||||
scale-info = { features = ["derive"], workspace = true }
|
||||
|
||||
pezframe-benchmarking = { optional = true, workspace = true }
|
||||
pezframe-support = { workspace = true }
|
||||
pezframe-system = { workspace = true }
|
||||
|
||||
pezsp-core = { workspace = true }
|
||||
pezsp-runtime = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
pezsp-io = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
runtime-benchmarks = [
|
||||
"pezframe-benchmarking/runtime-benchmarks",
|
||||
"pezframe-support/runtime-benchmarks",
|
||||
"pezframe-system/runtime-benchmarks",
|
||||
"pezsp-io/runtime-benchmarks",
|
||||
"pezsp-runtime/runtime-benchmarks",
|
||||
]
|
||||
try-runtime = [
|
||||
"pezframe-support/try-runtime",
|
||||
"pezframe-system/try-runtime",
|
||||
"pezsp-runtime/try-runtime",
|
||||
]
|
||||
std = [
|
||||
"codec/std",
|
||||
"pezframe-benchmarking/std",
|
||||
"pezframe-support/std",
|
||||
"pezframe-system/std",
|
||||
"scale-info/std",
|
||||
"pezsp-core/std",
|
||||
"pezsp-io/std",
|
||||
"pezsp-runtime/std",
|
||||
]
|
||||
@@ -0,0 +1,88 @@
|
||||
// Copyright (C) 2023 Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! The pallet benchmarks.
|
||||
|
||||
use super::{Pallet as CollectiveContent, *};
|
||||
use pezframe_benchmarking::v2::*;
|
||||
use pezframe_support::traits::EnsureOrigin;
|
||||
|
||||
fn assert_last_event<T: Config<I>, I: 'static>(generic_event: <T as Config<I>>::RuntimeEvent) {
|
||||
pezframe_system::Pallet::<T>::assert_last_event(generic_event.into());
|
||||
}
|
||||
|
||||
/// returns CID hash of 68 bytes of given `i`.
|
||||
fn create_cid(i: u8) -> OpaqueCid {
|
||||
let cid: OpaqueCid = [i; 68].to_vec().try_into().unwrap();
|
||||
cid
|
||||
}
|
||||
|
||||
#[instance_benchmarks]
|
||||
mod benchmarks {
|
||||
use super::*;
|
||||
|
||||
#[benchmark]
|
||||
fn set_charter() -> Result<(), BenchmarkError> {
|
||||
let cid: OpaqueCid = create_cid(1);
|
||||
let origin =
|
||||
T::CharterOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?;
|
||||
|
||||
#[extrinsic_call]
|
||||
_(origin as T::RuntimeOrigin, cid.clone());
|
||||
|
||||
assert_eq!(Charter::<T, I>::get(), Some(cid.clone()));
|
||||
assert_last_event::<T, I>(Event::NewCharterSet { cid }.into());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn announce() -> Result<(), BenchmarkError> {
|
||||
let expire_at = DispatchTime::<_>::At(10u32.into());
|
||||
let now = pezframe_system::Pallet::<T>::block_number();
|
||||
let cid: OpaqueCid = create_cid(1);
|
||||
let origin = T::AnnouncementOrigin::try_successful_origin()
|
||||
.map_err(|_| BenchmarkError::Weightless)?;
|
||||
|
||||
#[extrinsic_call]
|
||||
_(origin as T::RuntimeOrigin, cid.clone(), Some(expire_at));
|
||||
|
||||
assert_eq!(<Announcements<T, I>>::count(), 1);
|
||||
assert_last_event::<T, I>(
|
||||
Event::AnnouncementAnnounced { cid, expire_at: expire_at.evaluate(now) }.into(),
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn remove_announcement() -> Result<(), BenchmarkError> {
|
||||
let cid: OpaqueCid = create_cid(1);
|
||||
let origin = T::AnnouncementOrigin::try_successful_origin()
|
||||
.map_err(|_| BenchmarkError::Weightless)?;
|
||||
CollectiveContent::<T, I>::announce(origin.clone(), cid.clone(), None)
|
||||
.expect("could not publish an announcement");
|
||||
assert_eq!(<Announcements<T, I>>::count(), 1);
|
||||
|
||||
#[extrinsic_call]
|
||||
_(origin as T::RuntimeOrigin, cid.clone());
|
||||
|
||||
assert_eq!(<Announcements<T, I>>::count(), 0);
|
||||
assert_last_event::<T, I>(Event::AnnouncementRemoved { cid }.into());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl_benchmark_test_suite!(CollectiveContent, super::mock::new_bench_ext(), super::mock::Test);
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
// Copyright (C) 2023 Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Managed Collective Content Pallet
|
||||
//!
|
||||
//! The pallet provides the functionality to store different types of content. This would typically
|
||||
//! be used by an on-chain collective, such as the Pezkuwi Alliance or Ambassador Program.
|
||||
//!
|
||||
//! The pallet stores content as an [OpaqueCid], which should correspond to some off-chain hosting
|
||||
//! service, such as IPFS, and contain any type of data. Each type of content has its own origin
|
||||
//! from which it can be managed. The origins are configurable in the runtime. Storing content does
|
||||
//! not require a deposit, as it is expected to be managed by a trusted collective.
|
||||
//!
|
||||
//! Content types:
|
||||
//!
|
||||
//! - Collective [charter](pallet::Charter): A single document (`OpaqueCid`) managed by
|
||||
//! [CharterOrigin](pallet::Config::CharterOrigin).
|
||||
//! - Collective [announcements](pallet::Announcements): A list of announcements managed by
|
||||
//! [AnnouncementOrigin](pallet::Config::AnnouncementOrigin).
|
||||
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
#[cfg(test)]
|
||||
mod mock;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
mod benchmarking;
|
||||
pub mod weights;
|
||||
|
||||
pub use pallet::*;
|
||||
pub use weights::WeightInfo;
|
||||
|
||||
use pezframe_support::{traits::schedule::DispatchTime, BoundedVec};
|
||||
use pezsp_core::ConstU32;
|
||||
|
||||
/// IPFS compatible CID.
|
||||
// Worst case 2 bytes base and codec, 2 bytes hash type and size, 64 bytes hash digest.
|
||||
pub type OpaqueCid = BoundedVec<u8, ConstU32<68>>;
|
||||
|
||||
#[pezframe_support::pallet]
|
||||
pub mod pallet {
|
||||
use super::*;
|
||||
use pezframe_support::{ensure, pezpallet_prelude::*};
|
||||
use pezframe_system::pezpallet_prelude::*;
|
||||
use pezsp_runtime::{traits::BadOrigin, Saturating};
|
||||
|
||||
/// The in-code storage version.
|
||||
const STORAGE_VERSION: StorageVersion = StorageVersion::new(0);
|
||||
|
||||
#[pallet::pallet]
|
||||
#[pallet::storage_version(STORAGE_VERSION)]
|
||||
pub struct Pallet<T, I = ()>(PhantomData<(T, I)>);
|
||||
|
||||
/// The module configuration trait.
|
||||
#[pallet::config]
|
||||
pub trait Config<I: 'static = ()>: pezframe_system::Config {
|
||||
/// The overarching event type.
|
||||
#[allow(deprecated)]
|
||||
type RuntimeEvent: From<Event<Self, I>>
|
||||
+ IsType<<Self as pezframe_system::Config>::RuntimeEvent>;
|
||||
|
||||
/// Default lifetime for an announcement before it expires.
|
||||
type AnnouncementLifetime: Get<BlockNumberFor<Self>>;
|
||||
|
||||
/// The origin to control the collective announcements.
|
||||
type AnnouncementOrigin: EnsureOrigin<Self::RuntimeOrigin>;
|
||||
|
||||
/// Maximum number of announcements in the storage.
|
||||
#[pallet::constant]
|
||||
type MaxAnnouncements: Get<u32>;
|
||||
|
||||
/// The origin to control the collective charter.
|
||||
type CharterOrigin: EnsureOrigin<Self::RuntimeOrigin>;
|
||||
|
||||
/// Weight information needed for the pallet.
|
||||
type WeightInfo: WeightInfo;
|
||||
}
|
||||
|
||||
#[pallet::error]
|
||||
pub enum Error<T, I = ()> {
|
||||
/// The announcement is not found.
|
||||
MissingAnnouncement,
|
||||
/// Number of announcements exceeds `MaxAnnouncementsCount`.
|
||||
TooManyAnnouncements,
|
||||
/// Cannot expire in the past.
|
||||
InvalidExpiration,
|
||||
}
|
||||
|
||||
#[pallet::event]
|
||||
#[pallet::generate_deposit(pub(super) fn deposit_event)]
|
||||
pub enum Event<T: Config<I>, I: 'static = ()> {
|
||||
/// A new charter has been set.
|
||||
NewCharterSet { cid: OpaqueCid },
|
||||
/// A new announcement has been made.
|
||||
AnnouncementAnnounced { cid: OpaqueCid, expire_at: BlockNumberFor<T> },
|
||||
/// An on-chain announcement has been removed.
|
||||
AnnouncementRemoved { cid: OpaqueCid },
|
||||
}
|
||||
|
||||
/// The collective charter.
|
||||
#[pallet::storage]
|
||||
pub type Charter<T: Config<I>, I: 'static = ()> = StorageValue<_, OpaqueCid, OptionQuery>;
|
||||
|
||||
/// The collective announcements.
|
||||
#[pallet::storage]
|
||||
pub type Announcements<T: Config<I>, I: 'static = ()> =
|
||||
CountedStorageMap<_, Blake2_128Concat, OpaqueCid, BlockNumberFor<T>, OptionQuery>;
|
||||
|
||||
#[pallet::call]
|
||||
impl<T: Config<I>, I: 'static> Pallet<T, I> {
|
||||
/// Set the collective charter.
|
||||
///
|
||||
/// Parameters:
|
||||
/// - `origin`: Must be the [Config::CharterOrigin].
|
||||
/// - `cid`: [CID](super::OpaqueCid) of the IPFS document of the collective charter.
|
||||
#[pallet::call_index(0)]
|
||||
#[pallet::weight(T::WeightInfo::set_charter())]
|
||||
pub fn set_charter(origin: OriginFor<T>, cid: OpaqueCid) -> DispatchResult {
|
||||
T::CharterOrigin::ensure_origin(origin)?;
|
||||
|
||||
Charter::<T, I>::put(&cid);
|
||||
|
||||
Self::deposit_event(Event::<T, I>::NewCharterSet { cid });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Publish an announcement.
|
||||
///
|
||||
/// Parameters:
|
||||
/// - `origin`: Must be the [Config::AnnouncementOrigin].
|
||||
/// - `cid`: [CID](super::OpaqueCid) of the IPFS document to announce.
|
||||
/// - `maybe_expire`: Expiration block of the announcement. If `None`
|
||||
/// [`Config::AnnouncementLifetime`]
|
||||
/// used as a default.
|
||||
#[pallet::call_index(1)]
|
||||
#[pallet::weight(T::WeightInfo::announce())]
|
||||
pub fn announce(
|
||||
origin: OriginFor<T>,
|
||||
cid: OpaqueCid,
|
||||
maybe_expire: Option<DispatchTime<BlockNumberFor<T>>>,
|
||||
) -> DispatchResult {
|
||||
T::AnnouncementOrigin::ensure_origin(origin)?;
|
||||
|
||||
let now = pezframe_system::Pallet::<T>::block_number();
|
||||
let expire_at = maybe_expire
|
||||
.map_or(now.saturating_add(T::AnnouncementLifetime::get()), |e| e.evaluate(now));
|
||||
ensure!(expire_at > now, Error::<T, I>::InvalidExpiration);
|
||||
ensure!(
|
||||
T::MaxAnnouncements::get() > <Announcements<T, I>>::count(),
|
||||
Error::<T, I>::TooManyAnnouncements
|
||||
);
|
||||
|
||||
<Announcements<T, I>>::insert(cid.clone(), expire_at);
|
||||
|
||||
Self::deposit_event(Event::<T, I>::AnnouncementAnnounced { cid, expire_at });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove an announcement.
|
||||
///
|
||||
/// Transaction fee refunded for expired announcements.
|
||||
///
|
||||
/// Parameters:
|
||||
/// - `origin`: Must be the [Config::AnnouncementOrigin] or signed for expired
|
||||
/// announcements.
|
||||
/// - `cid`: [CID](super::OpaqueCid) of the IPFS document to remove.
|
||||
#[pallet::call_index(2)]
|
||||
#[pallet::weight(T::WeightInfo::remove_announcement())]
|
||||
pub fn remove_announcement(
|
||||
origin: OriginFor<T>,
|
||||
cid: OpaqueCid,
|
||||
) -> DispatchResultWithPostInfo {
|
||||
let maybe_who = match T::AnnouncementOrigin::try_origin(origin) {
|
||||
Ok(_) => None,
|
||||
Err(origin) => Some(ensure_signed(origin)?),
|
||||
};
|
||||
let expire_at = <Announcements<T, I>>::get(cid.clone())
|
||||
.ok_or(Error::<T, I>::MissingAnnouncement)?;
|
||||
let now = pezframe_system::Pallet::<T>::block_number();
|
||||
ensure!(maybe_who.is_none() || now >= expire_at, BadOrigin);
|
||||
|
||||
<Announcements<T, I>>::remove(cid.clone());
|
||||
|
||||
Self::deposit_event(Event::<T, I>::AnnouncementRemoved { cid });
|
||||
|
||||
if now >= expire_at {
|
||||
return Ok(Pays::No.into());
|
||||
}
|
||||
Ok(Pays::Yes.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
// Copyright (C) 2023 Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Test utilities.
|
||||
|
||||
pub use crate as pezpallet_collective_content;
|
||||
use crate::WeightInfo;
|
||||
use pezframe_support::{
|
||||
derive_impl, ord_parameter_types, parameter_types, traits::ConstU32, weights::Weight,
|
||||
};
|
||||
use pezframe_system::EnsureSignedBy;
|
||||
use pezsp_runtime::{traits::IdentityLookup, BuildStorage};
|
||||
|
||||
pezframe_support::construct_runtime!(
|
||||
pub enum Test {
|
||||
System: pezframe_system,
|
||||
CollectiveContent: pezpallet_collective_content,
|
||||
}
|
||||
);
|
||||
|
||||
type AccountId = u64;
|
||||
type Block = pezframe_system::mocking::MockBlock<Test>;
|
||||
|
||||
ord_parameter_types! {
|
||||
pub const CharterManager: u64 = 1;
|
||||
pub const AnnouncementManager: u64 = 2;
|
||||
pub const SomeAccount: u64 = 3;
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const AnnouncementLifetime: u64 = 100;
|
||||
pub const MaxAnnouncements: u32 = 5;
|
||||
}
|
||||
|
||||
impl pezpallet_collective_content::Config for Test {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type AnnouncementLifetime = AnnouncementLifetime;
|
||||
type AnnouncementOrigin = EnsureSignedBy<AnnouncementManager, AccountId>;
|
||||
type MaxAnnouncements = MaxAnnouncements;
|
||||
type CharterOrigin = EnsureSignedBy<CharterManager, AccountId>;
|
||||
type WeightInfo = CCWeightInfo;
|
||||
}
|
||||
|
||||
#[derive_impl(pezframe_system::config_preludes::TestDefaultConfig)]
|
||||
impl pezframe_system::Config for Test {
|
||||
type BaseCallFilter = ();
|
||||
type BlockWeights = ();
|
||||
type BlockLength = ();
|
||||
type DbWeight = ();
|
||||
type RuntimeOrigin = RuntimeOrigin;
|
||||
type RuntimeCall = RuntimeCall;
|
||||
type Nonce = u64;
|
||||
type Block = Block;
|
||||
type Hash = pezsp_core::H256;
|
||||
type Hashing = pezsp_runtime::traits::BlakeTwo256;
|
||||
type AccountId = AccountId;
|
||||
type Lookup = IdentityLookup<Self::AccountId>;
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type Version = ();
|
||||
type PalletInfo = PalletInfo;
|
||||
type AccountData = ();
|
||||
type OnNewAccount = ();
|
||||
type OnKilledAccount = ();
|
||||
type SystemWeightInfo = ();
|
||||
type SS58Prefix = ();
|
||||
type OnSetCode = ();
|
||||
type MaxConsumers = ConstU32<16>;
|
||||
}
|
||||
pub struct CCWeightInfo;
|
||||
impl WeightInfo for CCWeightInfo {
|
||||
fn set_charter() -> Weight {
|
||||
Weight::zero()
|
||||
}
|
||||
fn announce() -> Weight {
|
||||
Weight::zero()
|
||||
}
|
||||
fn remove_announcement() -> Weight {
|
||||
Weight::zero()
|
||||
}
|
||||
}
|
||||
|
||||
// Build test environment.
|
||||
pub fn new_test_ext() -> pezsp_io::TestExternalities {
|
||||
let t = RuntimeGenesisConfig::default().build_storage().unwrap().into();
|
||||
let mut ext = pezsp_io::TestExternalities::new(t);
|
||||
ext.execute_with(|| System::set_block_number(1));
|
||||
ext
|
||||
}
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
pub fn new_bench_ext() -> pezsp_io::TestExternalities {
|
||||
RuntimeGenesisConfig::default().build_storage().unwrap().into()
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
// Copyright (C) 2023 Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Tests.
|
||||
|
||||
use super::{mock::*, *};
|
||||
use pezframe_support::{assert_noop, assert_ok, pezpallet_prelude::Pays};
|
||||
use pezsp_runtime::traits::BadOrigin;
|
||||
|
||||
/// returns CID hash of 68 bytes of given `i`.
|
||||
fn create_cid(i: u8) -> OpaqueCid {
|
||||
let cid: OpaqueCid = [i; 68].to_vec().try_into().unwrap();
|
||||
cid
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_charter_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// wrong origin.
|
||||
let origin = RuntimeOrigin::signed(SomeAccount::get());
|
||||
let cid = create_cid(1);
|
||||
assert_noop!(CollectiveContent::set_charter(origin, cid), BadOrigin);
|
||||
|
||||
// success.
|
||||
let origin = RuntimeOrigin::signed(CharterManager::get());
|
||||
let cid = create_cid(2);
|
||||
|
||||
assert_ok!(CollectiveContent::set_charter(origin, cid.clone()));
|
||||
assert_eq!(Charter::<Test, _>::get(), Some(cid.clone()));
|
||||
System::assert_last_event(RuntimeEvent::CollectiveContent(Event::NewCharterSet { cid }));
|
||||
|
||||
// reset. success.
|
||||
let origin = RuntimeOrigin::signed(CharterManager::get());
|
||||
let cid = create_cid(3);
|
||||
|
||||
assert_ok!(CollectiveContent::set_charter(origin, cid.clone()));
|
||||
assert_eq!(Charter::<Test, _>::get(), Some(cid.clone()));
|
||||
System::assert_last_event(RuntimeEvent::CollectiveContent(Event::NewCharterSet { cid }));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn announce_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let now = pezframe_system::Pallet::<Test>::block_number();
|
||||
// wrong origin.
|
||||
let origin = RuntimeOrigin::signed(SomeAccount::get());
|
||||
let cid = create_cid(1);
|
||||
|
||||
assert_noop!(CollectiveContent::announce(origin, cid, None), BadOrigin);
|
||||
|
||||
// success.
|
||||
let origin = RuntimeOrigin::signed(AnnouncementManager::get());
|
||||
let cid = create_cid(2);
|
||||
let maybe_expire_at = None;
|
||||
|
||||
assert_ok!(CollectiveContent::announce(origin, cid.clone(), maybe_expire_at));
|
||||
System::assert_last_event(RuntimeEvent::CollectiveContent(Event::AnnouncementAnnounced {
|
||||
cid,
|
||||
expire_at: now.saturating_add(AnnouncementLifetime::get()),
|
||||
}));
|
||||
|
||||
// one more. success.
|
||||
let origin = RuntimeOrigin::signed(AnnouncementManager::get());
|
||||
let cid = create_cid(3);
|
||||
let maybe_expire_at = None;
|
||||
|
||||
assert_ok!(CollectiveContent::announce(origin, cid.clone(), maybe_expire_at));
|
||||
System::assert_last_event(RuntimeEvent::CollectiveContent(Event::AnnouncementAnnounced {
|
||||
cid,
|
||||
expire_at: now.saturating_add(AnnouncementLifetime::get()),
|
||||
}));
|
||||
|
||||
// one more with expire. success.
|
||||
let origin = RuntimeOrigin::signed(AnnouncementManager::get());
|
||||
let cid = create_cid(4);
|
||||
let maybe_expire_at = DispatchTime::<_>::After(10);
|
||||
|
||||
assert_ok!(CollectiveContent::announce(origin, cid.clone(), Some(maybe_expire_at)));
|
||||
System::assert_last_event(RuntimeEvent::CollectiveContent(Event::AnnouncementAnnounced {
|
||||
cid,
|
||||
expire_at: maybe_expire_at.evaluate(now),
|
||||
}));
|
||||
|
||||
// one more with later expire. success.
|
||||
let origin = RuntimeOrigin::signed(AnnouncementManager::get());
|
||||
let cid = create_cid(5);
|
||||
let maybe_expire_at = DispatchTime::<_>::At(now + 20);
|
||||
|
||||
assert_ok!(CollectiveContent::announce(origin, cid.clone(), Some(maybe_expire_at)));
|
||||
System::assert_last_event(RuntimeEvent::CollectiveContent(Event::AnnouncementAnnounced {
|
||||
cid,
|
||||
expire_at: maybe_expire_at.evaluate(now),
|
||||
}));
|
||||
|
||||
// one more with earlier expire. success.
|
||||
let origin = RuntimeOrigin::signed(AnnouncementManager::get());
|
||||
let cid = create_cid(6);
|
||||
let maybe_expire_at = DispatchTime::<_>::At(now + 5);
|
||||
|
||||
assert_ok!(CollectiveContent::announce(origin, cid.clone(), Some(maybe_expire_at)));
|
||||
System::assert_last_event(RuntimeEvent::CollectiveContent(Event::AnnouncementAnnounced {
|
||||
cid,
|
||||
expire_at: maybe_expire_at.evaluate(now),
|
||||
}));
|
||||
|
||||
// one more with earlier expire. success.
|
||||
let origin = RuntimeOrigin::signed(AnnouncementManager::get());
|
||||
let cid = create_cid(7);
|
||||
let maybe_expire_at = DispatchTime::<_>::At(now + 5);
|
||||
|
||||
assert_eq!(<Announcements<Test, _>>::count(), MaxAnnouncements::get());
|
||||
assert_noop!(
|
||||
CollectiveContent::announce(origin, cid.clone(), Some(maybe_expire_at)),
|
||||
Error::<Test>::TooManyAnnouncements
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_announcement_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// wrong origin.
|
||||
let origin = RuntimeOrigin::signed(CharterManager::get());
|
||||
let cid = create_cid(8);
|
||||
|
||||
assert_noop!(
|
||||
CollectiveContent::remove_announcement(origin, cid),
|
||||
Error::<Test>::MissingAnnouncement
|
||||
);
|
||||
|
||||
// missing announcement.
|
||||
let origin = RuntimeOrigin::signed(AnnouncementManager::get());
|
||||
let cid = create_cid(9);
|
||||
|
||||
assert_noop!(
|
||||
CollectiveContent::remove_announcement(origin, cid),
|
||||
Error::<Test>::MissingAnnouncement
|
||||
);
|
||||
|
||||
// wrong origin. announcement not yet expired.
|
||||
let origin = RuntimeOrigin::signed(AnnouncementManager::get());
|
||||
let cid = create_cid(10);
|
||||
assert_ok!(CollectiveContent::announce(origin.clone(), cid.clone(), None));
|
||||
assert!(<Announcements<Test>>::contains_key(cid.clone()));
|
||||
|
||||
let origin = RuntimeOrigin::signed(SomeAccount::get());
|
||||
assert_noop!(CollectiveContent::remove_announcement(origin, cid.clone()), BadOrigin);
|
||||
let origin = RuntimeOrigin::signed(AnnouncementManager::get());
|
||||
assert_ok!(CollectiveContent::remove_announcement(origin, cid));
|
||||
|
||||
// success.
|
||||
|
||||
// remove first announcement and assert.
|
||||
let origin = RuntimeOrigin::signed(AnnouncementManager::get());
|
||||
let cid = create_cid(11);
|
||||
assert_ok!(CollectiveContent::announce(origin.clone(), cid.clone(), None));
|
||||
assert!(<Announcements<Test>>::contains_key(cid.clone()));
|
||||
|
||||
let info = CollectiveContent::remove_announcement(origin.clone(), cid.clone()).unwrap();
|
||||
assert_eq!(info.pays_fee, Pays::Yes);
|
||||
System::assert_last_event(RuntimeEvent::CollectiveContent(Event::AnnouncementRemoved {
|
||||
cid: cid.clone(),
|
||||
}));
|
||||
assert_noop!(
|
||||
CollectiveContent::remove_announcement(origin, cid.clone()),
|
||||
Error::<Test>::MissingAnnouncement
|
||||
);
|
||||
assert!(!<Announcements<Test>>::contains_key(cid));
|
||||
|
||||
// remove expired announcement and assert.
|
||||
let origin = RuntimeOrigin::signed(AnnouncementManager::get());
|
||||
let cid = create_cid(12);
|
||||
assert_ok!(CollectiveContent::announce(
|
||||
origin.clone(),
|
||||
cid.clone(),
|
||||
Some(DispatchTime::<_>::At(10))
|
||||
));
|
||||
assert!(<Announcements<Test>>::contains_key(cid.clone()));
|
||||
|
||||
System::set_block_number(11);
|
||||
let origin = RuntimeOrigin::signed(SomeAccount::get());
|
||||
let info = CollectiveContent::remove_announcement(origin.clone(), cid.clone()).unwrap();
|
||||
assert_eq!(info.pays_fee, Pays::No);
|
||||
System::assert_last_event(RuntimeEvent::CollectiveContent(Event::AnnouncementRemoved {
|
||||
cid: cid.clone(),
|
||||
}));
|
||||
assert_noop!(
|
||||
CollectiveContent::remove_announcement(origin, cid),
|
||||
Error::<Test>::MissingAnnouncement
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright (C) 2023 Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! The pallet weight info trait and its unit implementation.
|
||||
|
||||
use pezframe_support::weights::Weight;
|
||||
|
||||
/// Weights information needed for the pallet.
|
||||
pub trait WeightInfo {
|
||||
/// Returns the weight of the set_charter extrinsic.
|
||||
fn set_charter() -> Weight;
|
||||
/// Returns the weight of the announce extrinsic.
|
||||
fn announce() -> Weight;
|
||||
/// Returns the weight of the remove_announcement extrinsic.
|
||||
fn remove_announcement() -> Weight;
|
||||
}
|
||||
|
||||
/// Unit implementation of the [WeightInfo].
|
||||
impl WeightInfo for () {
|
||||
fn set_charter() -> Weight {
|
||||
Weight::zero()
|
||||
}
|
||||
fn announce() -> Weight {
|
||||
Weight::zero()
|
||||
}
|
||||
fn remove_announcement() -> Weight {
|
||||
Weight::zero()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
[package]
|
||||
name = "pezpallet-identity-kyc"
|
||||
version = "1.0.0"
|
||||
description = "PezkuwiChain Identity and KYC Management Pallet"
|
||||
authors.workspace = true
|
||||
homepage.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
publish = false
|
||||
repository.workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
||||
[dependencies]
|
||||
codec = { workspace = true, default-features = false, features = ["derive"] }
|
||||
pezframe-benchmarking = { optional = true, workspace = true }
|
||||
pezframe-support = { default-features = false, workspace = true }
|
||||
pezframe-system = { default-features = false, workspace = true }
|
||||
log = { default-features = false, workspace = true }
|
||||
scale-info = { default-features = false, features = [
|
||||
"derive",
|
||||
], workspace = true }
|
||||
pezsp-core = { default-features = false, workspace = true }
|
||||
pezsp-runtime = { default-features = false, workspace = true }
|
||||
pezsp-std = { default-features = false, workspace = true }
|
||||
|
||||
# Projemizin özel tiplerini ve trait'lerini içeren kütüphane
|
||||
pezkuwi-primitives = { workspace = true, default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
pezpallet-balances = { workspace = true }
|
||||
pezsp-io = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = [
|
||||
"codec/std",
|
||||
"pezframe-benchmarking?/std",
|
||||
"pezframe-support/std",
|
||||
"pezframe-system/std",
|
||||
"log/std",
|
||||
"pezpallet-balances/std",
|
||||
"pezkuwi-primitives/std",
|
||||
"scale-info/std",
|
||||
"pezsp-core/std",
|
||||
"pezsp-io/std",
|
||||
"pezsp-runtime/std",
|
||||
"pezsp-std/std",
|
||||
]
|
||||
runtime-benchmarks = [
|
||||
"pezframe-benchmarking/runtime-benchmarks",
|
||||
"pezframe-support/runtime-benchmarks",
|
||||
"pezframe-system/runtime-benchmarks",
|
||||
"pezpallet-balances/runtime-benchmarks",
|
||||
"pezkuwi-primitives/runtime-benchmarks",
|
||||
"pezsp-io/runtime-benchmarks",
|
||||
"pezsp-runtime/runtime-benchmarks",
|
||||
]
|
||||
|
||||
try-runtime = [
|
||||
"pezframe-support/try-runtime",
|
||||
"pezframe-system/try-runtime",
|
||||
"pezpallet-balances/try-runtime",
|
||||
"pezsp-runtime/try-runtime",
|
||||
]
|
||||
@@ -0,0 +1,136 @@
|
||||
//! Benchmarking setup for pezpallet-identity-kyc
|
||||
|
||||
#![cfg(feature = "runtime-benchmarks")]
|
||||
|
||||
use super::*;
|
||||
use crate::Pallet as IdentityKyc;
|
||||
use pezframe_benchmarking::v2::*;
|
||||
use pezframe_support::traits::Currency;
|
||||
use pezframe_system::RawOrigin;
|
||||
use pezsp_core::H256;
|
||||
|
||||
/// Helper function to create a funded account
|
||||
fn funded_account<T: Config>(name: &'static str, index: u32) -> T::AccountId {
|
||||
let caller: T::AccountId = account(name, index, 0);
|
||||
let amount = T::KycApplicationDeposit::get() * 10u32.into();
|
||||
T::Currency::make_free_balance_be(&caller, amount);
|
||||
caller
|
||||
}
|
||||
|
||||
/// Helper function to setup a citizen (for referrer)
|
||||
fn setup_citizen<T: Config>(who: &T::AccountId) {
|
||||
KycStatuses::<T>::insert(who, KycLevel::Approved);
|
||||
}
|
||||
|
||||
/// Helper function to setup an applicant in PendingReferral state
|
||||
fn setup_pending_referral<T: Config>(applicant: &T::AccountId, referrer: &T::AccountId) {
|
||||
let identity_hash = H256::repeat_byte(0x01);
|
||||
let application = CitizenshipApplication { identity_hash, referrer: referrer.clone() };
|
||||
Applications::<T>::insert(applicant, application);
|
||||
KycStatuses::<T>::insert(applicant, KycLevel::PendingReferral);
|
||||
|
||||
// Reserve deposit
|
||||
let deposit = T::KycApplicationDeposit::get();
|
||||
let _ = T::Currency::reserve(applicant, deposit);
|
||||
}
|
||||
|
||||
/// Helper function to setup an applicant in ReferrerApproved state
|
||||
fn setup_referrer_approved<T: Config>(applicant: &T::AccountId, referrer: &T::AccountId) {
|
||||
let identity_hash = H256::repeat_byte(0x01);
|
||||
let application = CitizenshipApplication { identity_hash, referrer: referrer.clone() };
|
||||
Applications::<T>::insert(applicant, application);
|
||||
KycStatuses::<T>::insert(applicant, KycLevel::ReferrerApproved);
|
||||
|
||||
// Reserve deposit
|
||||
let deposit = T::KycApplicationDeposit::get();
|
||||
let _ = T::Currency::reserve(applicant, deposit);
|
||||
}
|
||||
|
||||
#[benchmarks]
|
||||
mod benchmarks {
|
||||
use super::*;
|
||||
|
||||
#[benchmark]
|
||||
fn apply_for_citizenship() {
|
||||
let referrer: T::AccountId = funded_account::<T>("referrer", 0);
|
||||
setup_citizen::<T>(&referrer);
|
||||
|
||||
let applicant: T::AccountId = funded_account::<T>("applicant", 1);
|
||||
let identity_hash = H256::repeat_byte(0x42);
|
||||
|
||||
#[extrinsic_call]
|
||||
apply_for_citizenship(
|
||||
RawOrigin::Signed(applicant.clone()),
|
||||
identity_hash,
|
||||
referrer.clone(),
|
||||
);
|
||||
|
||||
assert_eq!(KycStatuses::<T>::get(&applicant), KycLevel::PendingReferral);
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn approve_referral() {
|
||||
let referrer: T::AccountId = funded_account::<T>("referrer", 0);
|
||||
setup_citizen::<T>(&referrer);
|
||||
|
||||
let applicant: T::AccountId = funded_account::<T>("applicant", 1);
|
||||
setup_pending_referral::<T>(&applicant, &referrer);
|
||||
|
||||
#[extrinsic_call]
|
||||
approve_referral(RawOrigin::Signed(referrer.clone()), applicant.clone());
|
||||
|
||||
assert_eq!(KycStatuses::<T>::get(&applicant), KycLevel::ReferrerApproved);
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn confirm_citizenship() {
|
||||
let referrer: T::AccountId = funded_account::<T>("referrer", 0);
|
||||
setup_citizen::<T>(&referrer);
|
||||
|
||||
let applicant: T::AccountId = funded_account::<T>("applicant", 1);
|
||||
setup_referrer_approved::<T>(&applicant, &referrer);
|
||||
|
||||
#[extrinsic_call]
|
||||
confirm_citizenship(RawOrigin::Signed(applicant.clone()));
|
||||
|
||||
assert_eq!(KycStatuses::<T>::get(&applicant), KycLevel::Approved);
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn revoke_citizenship() {
|
||||
let citizen: T::AccountId = funded_account::<T>("citizen", 0);
|
||||
setup_citizen::<T>(&citizen);
|
||||
|
||||
#[extrinsic_call]
|
||||
revoke_citizenship(RawOrigin::Root, citizen.clone());
|
||||
|
||||
assert_eq!(KycStatuses::<T>::get(&citizen), KycLevel::Revoked);
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn renounce_citizenship() {
|
||||
let citizen: T::AccountId = funded_account::<T>("citizen", 0);
|
||||
setup_citizen::<T>(&citizen);
|
||||
|
||||
#[extrinsic_call]
|
||||
renounce_citizenship(RawOrigin::Signed(citizen.clone()));
|
||||
|
||||
assert_eq!(KycStatuses::<T>::get(&citizen), KycLevel::NotStarted);
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn cancel_application() {
|
||||
let referrer: T::AccountId = funded_account::<T>("referrer", 0);
|
||||
setup_citizen::<T>(&referrer);
|
||||
|
||||
let applicant: T::AccountId = funded_account::<T>("applicant", 1);
|
||||
setup_pending_referral::<T>(&applicant, &referrer);
|
||||
|
||||
#[extrinsic_call]
|
||||
cancel_application(RawOrigin::Signed(applicant.clone()));
|
||||
|
||||
assert_eq!(KycStatuses::<T>::get(&applicant), KycLevel::NotStarted);
|
||||
}
|
||||
|
||||
impl_benchmark_test_suite!(IdentityKyc, crate::mock::new_test_ext(), crate::mock::Test);
|
||||
}
|
||||
@@ -0,0 +1,563 @@
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
//! # Identity & KYC Pallet - TRUSTLESS MODEL
|
||||
//!
|
||||
//! A privacy-preserving, decentralized citizenship verification system.
|
||||
//!
|
||||
//! ## Overview
|
||||
//!
|
||||
//! This pallet implements a **TRUSTLESS** citizenship verification where:
|
||||
//! - NO personal data is stored on-chain (only hash)
|
||||
//! - NO central authority/bot approves applications
|
||||
//! - Existing citizens vouch for new applicants (referral-based)
|
||||
//! - Direct responsibility: Referrers are accountable for their referrals
|
||||
//!
|
||||
//! ## Security Design (Kurdish People Safety)
|
||||
//!
|
||||
//! This system is designed to protect vulnerable populations (like Kurdish people)
|
||||
//! from hostile regimes that might try to identify applicants:
|
||||
//! - Only H256 hash of identity stored on-chain
|
||||
//! - Actual documents stored off-chain (IPFS/encrypted)
|
||||
//! - No admin can see or leak personal data
|
||||
//! - Referral chain creates accountability without central authority
|
||||
//!
|
||||
//! ## Citizenship Workflow
|
||||
//!
|
||||
//! ### 1. Application Phase
|
||||
//! - User creates identity hash off-chain: `H256(name + email + documents)`
|
||||
//! - User calls `apply_for_citizenship(identity_hash, referrer_account)`
|
||||
//! - Referrer MUST be an existing citizen (KycLevel::Approved)
|
||||
//! - Status changes to `PendingReferral`
|
||||
//!
|
||||
//! ### 2. Referrer Approval Phase
|
||||
//! - Referrer reviews applicant (off-chain verification)
|
||||
//! - Referrer calls `approve_referral(applicant)` to vouch for them
|
||||
//! - Status changes to `ReferrerApproved`
|
||||
//! - Referrer takes personal responsibility for this referral
|
||||
//!
|
||||
//! ### 3. Self-Confirmation Phase (Welati NFT Only)
|
||||
//! - Applicant calls `confirm_citizenship()` to complete the process
|
||||
//! - Status changes to `Approved`
|
||||
//! - Citizen NFT (Welati) is minted via self-confirmation
|
||||
//! - Referral hooks are triggered
|
||||
//!
|
||||
//! ## KYC Levels
|
||||
//!
|
||||
//! - **NotStarted** - No application submitted
|
||||
//! - **PendingReferral** - Waiting for referrer approval
|
||||
//! - **ReferrerApproved** - Referrer approved, ready for self-confirmation
|
||||
//! - **Approved** - Full citizen with all rights
|
||||
//! - **Revoked** - Citizenship revoked (governance decision)
|
||||
//!
|
||||
//! ## Privacy Features
|
||||
//!
|
||||
//! - **Hash-only storage**: No personal data on-chain
|
||||
//! - **Off-chain documents**: IPFS or encrypted storage
|
||||
//! - **No admin access**: Decentralized verification
|
||||
//! - **Referral accountability**: Social trust, not central authority
|
||||
//!
|
||||
//! ## Direct Responsibility Model
|
||||
//!
|
||||
//! When a citizen is found to be malicious:
|
||||
//! - ONLY their direct referrer is penalized
|
||||
//! - Penalty: Trust score reduction + potential citizenship review
|
||||
//! - Chain reactions are limited to direct relationships
|
||||
//! - Good referrals from bad actors are NOT penalized
|
||||
//!
|
||||
//! ## Interface
|
||||
//!
|
||||
//! ### User Extrinsics
|
||||
//!
|
||||
//! - `apply_for_citizenship(identity_hash, referrer)` - Submit citizenship application
|
||||
//! - `confirm_citizenship()` - Self-confirm after referrer approval (Welati only)
|
||||
//! - `renounce_citizenship()` - Voluntarily give up citizenship
|
||||
//!
|
||||
//! ### Referrer Extrinsics
|
||||
//!
|
||||
//! - `approve_referral(applicant)` - Vouch for an applicant
|
||||
//!
|
||||
//! ### Governance Extrinsics (Root only)
|
||||
//!
|
||||
//! - `revoke_citizenship(who)` - Revoke citizenship (governance decision)
|
||||
//!
|
||||
//! ## Runtime Integration Example
|
||||
//!
|
||||
//! ```ignore
|
||||
//! impl pezpallet_identity_kyc::Config for Runtime {
|
||||
//! type RuntimeEvent = RuntimeEvent;
|
||||
//! type Currency = Balances;
|
||||
//! type WeightInfo = pezpallet_identity_kyc::weights::BizinikiwiWeight<Runtime>;
|
||||
//! type OnKycApproved = Referral;
|
||||
//! type CitizenNftProvider = Tiki;
|
||||
//! type KycApplicationDeposit = ConstU128<1_000_000_000_000>; // Spam prevention
|
||||
//! type MaxStringLength = ConstU32<128>;
|
||||
//! type MaxCidLength = ConstU32<64>;
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
pub use pallet::*;
|
||||
pub mod types;
|
||||
use types::*;
|
||||
pub mod weights;
|
||||
pub use weights::WeightInfo;
|
||||
|
||||
#[cfg(test)]
|
||||
mod mock;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
mod benchmarking;
|
||||
|
||||
extern crate alloc;
|
||||
use pezframe_support::{pezpallet_prelude::*, traits::ReservableCurrency};
|
||||
use pezframe_system::pezpallet_prelude::*;
|
||||
use pezsp_core::H256;
|
||||
|
||||
#[pezframe_support::pallet]
|
||||
pub mod pallet {
|
||||
use super::*;
|
||||
|
||||
#[pallet::pallet]
|
||||
pub struct Pallet<T>(_);
|
||||
|
||||
#[pallet::config]
|
||||
pub trait Config: pezframe_system::Config {
|
||||
type RuntimeEvent: From<Event<Self>> + IsType<<Self as pezframe_system::Config>::RuntimeEvent>;
|
||||
type Currency: ReservableCurrency<Self::AccountId>;
|
||||
|
||||
/// Origin that can revoke citizenship (governance/root)
|
||||
type GovernanceOrigin: EnsureOrigin<Self::RuntimeOrigin>;
|
||||
|
||||
type WeightInfo: WeightInfo;
|
||||
|
||||
/// Hook called when citizenship is approved - used by referral pallet
|
||||
type OnKycApproved: crate::types::OnKycApproved<Self::AccountId>;
|
||||
|
||||
/// Hook called when citizenship is revoked - used by referral pallet for penalty
|
||||
type OnCitizenshipRevoked: crate::types::OnCitizenshipRevoked<Self::AccountId>;
|
||||
|
||||
/// Provider for minting citizen NFTs - used by tiki pallet
|
||||
type CitizenNftProvider: crate::types::CitizenNftProvider<Self::AccountId>;
|
||||
|
||||
/// Deposit required to apply (spam prevention, returned on approval)
|
||||
#[pallet::constant]
|
||||
type KycApplicationDeposit: Get<BalanceOf<Self>>;
|
||||
|
||||
/// Max string length for legacy storage
|
||||
#[pallet::constant]
|
||||
type MaxStringLength: Get<u32>;
|
||||
|
||||
/// Max CID length for legacy storage
|
||||
#[pallet::constant]
|
||||
type MaxCidLength: Get<u32>;
|
||||
}
|
||||
|
||||
pub type BalanceOf<T> = <<T as Config>::Currency as pezframe_support::traits::Currency<
|
||||
<T as pezframe_system::Config>::AccountId,
|
||||
>>::Balance;
|
||||
|
||||
// ============= STORAGE =============
|
||||
|
||||
/// Citizenship applications (applicant -> application)
|
||||
/// PRIVACY: Only hash stored, no personal data
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn applications)]
|
||||
pub type Applications<T: Config> =
|
||||
StorageMap<_, Blake2_128Concat, T::AccountId, CitizenshipApplication<T::AccountId>>;
|
||||
|
||||
/// Current citizenship status per account
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn kyc_status_of)]
|
||||
pub type KycStatuses<T: Config> =
|
||||
StorageMap<_, Blake2_128Concat, T::AccountId, KycLevel, ValueQuery>;
|
||||
|
||||
/// Identity hashes of approved citizens (for verification)
|
||||
/// Can be used to prove citizenship without revealing identity
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn identity_hash_of)]
|
||||
pub type IdentityHashes<T: Config> = StorageMap<_, Blake2_128Concat, T::AccountId, H256>;
|
||||
|
||||
/// Referrer of approved citizens (for direct responsibility tracking)
|
||||
/// Kept permanently for penalty system even after application is removed
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn citizen_referrer)]
|
||||
pub type CitizenReferrers<T: Config> =
|
||||
StorageMap<_, Blake2_128Concat, T::AccountId, T::AccountId>;
|
||||
|
||||
// ============= LEGACY STORAGE (for migration) =============
|
||||
|
||||
/// Legacy: Identity info storage (deprecated, kept for migration)
|
||||
#[pallet::storage]
|
||||
pub type Identities<T: Config> =
|
||||
StorageMap<_, Blake2_128Concat, T::AccountId, IdentityInfo<T::MaxStringLength>>;
|
||||
|
||||
/// Legacy: Pending KYC applications (deprecated, kept for migration)
|
||||
#[pallet::storage]
|
||||
pub type PendingKycApplications<T: Config> = StorageMap<
|
||||
_,
|
||||
Blake2_128Concat,
|
||||
T::AccountId,
|
||||
KycApplication<T::MaxStringLength, T::MaxCidLength>,
|
||||
>;
|
||||
|
||||
// ============= GENESIS CONFIG =============
|
||||
|
||||
/// Genesis configuration for bootstrapping initial citizens
|
||||
/// BOOTSTRAP: Solves chicken-egg problem - first citizens need to exist for others to join
|
||||
#[pallet::genesis_config]
|
||||
#[derive(pezframe_support::DefaultNoBound)]
|
||||
pub struct GenesisConfig<T: Config> {
|
||||
/// List of founding citizens (AccountId, IdentityHash)
|
||||
/// These accounts start with Approved status and can accept referrals immediately
|
||||
pub founding_citizens: alloc::vec::Vec<(T::AccountId, H256)>,
|
||||
#[serde(skip)]
|
||||
pub _phantom: core::marker::PhantomData<T>,
|
||||
}
|
||||
|
||||
#[pallet::genesis_build]
|
||||
impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
|
||||
fn build(&self) {
|
||||
// Initialize founding citizens with Approved status
|
||||
for (account, identity_hash) in &self.founding_citizens {
|
||||
// Set status to Approved (citizen)
|
||||
KycStatuses::<T>::insert(account, KycLevel::Approved);
|
||||
// Store identity hash
|
||||
IdentityHashes::<T>::insert(account, *identity_hash);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============= EVENTS =============
|
||||
|
||||
#[pallet::event]
|
||||
#[pallet::generate_deposit(pub(super) fn deposit_event)]
|
||||
pub enum Event<T: Config> {
|
||||
/// New citizenship application submitted
|
||||
CitizenshipApplied { applicant: T::AccountId, referrer: T::AccountId, identity_hash: H256 },
|
||||
/// Referrer approved the application
|
||||
ReferralApproved { referrer: T::AccountId, applicant: T::AccountId },
|
||||
/// Applicant self-confirmed their citizenship (Welati NFT minted)
|
||||
CitizenshipConfirmed { who: T::AccountId },
|
||||
/// Citizenship was revoked (by governance)
|
||||
CitizenshipRevoked { who: T::AccountId },
|
||||
/// User renounced their citizenship
|
||||
CitizenshipRenounced { who: T::AccountId },
|
||||
/// Application was cancelled by the applicant
|
||||
ApplicationCancelled { who: T::AccountId },
|
||||
}
|
||||
|
||||
// ============= ERRORS =============
|
||||
|
||||
#[pallet::error]
|
||||
pub enum Error<T> {
|
||||
/// Application already exists for this account
|
||||
ApplicationAlreadyExists,
|
||||
/// No application found for this account
|
||||
ApplicationNotFound,
|
||||
/// Referrer is not a citizen (must have Approved status)
|
||||
ReferrerNotCitizen,
|
||||
/// Cannot refer yourself
|
||||
SelfReferral,
|
||||
/// Cannot approve referral in current state (must be PendingReferral)
|
||||
CannotApproveInCurrentState,
|
||||
/// Cannot confirm in current state (must be ReferrerApproved)
|
||||
CannotConfirmInCurrentState,
|
||||
/// Cannot revoke in current state (must be Approved)
|
||||
CannotRevokeInCurrentState,
|
||||
/// User is not a citizen (cannot renounce)
|
||||
NotACitizen,
|
||||
/// Only the referrer can approve this application
|
||||
NotTheReferrer,
|
||||
/// Cannot cancel application in current state (must be PendingReferral)
|
||||
CannotCancelInCurrentState,
|
||||
}
|
||||
|
||||
// ============= EXTRINSICS =============
|
||||
|
||||
#[pallet::call]
|
||||
impl<T: Config> Pallet<T> {
|
||||
/// Apply for citizenship with identity hash and referrer
|
||||
///
|
||||
/// TRUSTLESS: No admin involved, referrer vouches for applicant
|
||||
/// PRIVACY: Only hash stored, actual identity is off-chain
|
||||
///
|
||||
/// # Arguments
|
||||
/// - `identity_hash`: H256 hash of identity documents (calculated off-chain)
|
||||
/// - `referrer`: Account of existing citizen who will vouch for you
|
||||
///
|
||||
/// # Workflow
|
||||
/// 1. Applicant submits hash + referrer
|
||||
/// 2. Deposit is reserved (spam prevention)
|
||||
/// 3. Status becomes PendingReferral
|
||||
/// 4. Referrer must call approve_referral
|
||||
#[pallet::call_index(0)]
|
||||
#[pallet::weight(T::WeightInfo::apply_for_citizenship())]
|
||||
pub fn apply_for_citizenship(
|
||||
origin: OriginFor<T>,
|
||||
identity_hash: H256,
|
||||
referrer: T::AccountId,
|
||||
) -> DispatchResult {
|
||||
let applicant = ensure_signed(origin)?;
|
||||
|
||||
// Cannot refer yourself
|
||||
ensure!(applicant != referrer, Error::<T>::SelfReferral);
|
||||
|
||||
// Must not have existing application
|
||||
ensure!(
|
||||
KycStatuses::<T>::get(&applicant) == KycLevel::NotStarted,
|
||||
Error::<T>::ApplicationAlreadyExists
|
||||
);
|
||||
|
||||
// Referrer must be an approved citizen
|
||||
ensure!(
|
||||
KycStatuses::<T>::get(&referrer) == KycLevel::Approved,
|
||||
Error::<T>::ReferrerNotCitizen
|
||||
);
|
||||
|
||||
// Reserve deposit (spam prevention, returned on approval)
|
||||
let deposit = T::KycApplicationDeposit::get();
|
||||
T::Currency::reserve(&applicant, deposit)?;
|
||||
|
||||
// Store application (only hash, no personal data)
|
||||
let application = CitizenshipApplication { identity_hash, referrer: referrer.clone() };
|
||||
Applications::<T>::insert(&applicant, application);
|
||||
|
||||
// Update status
|
||||
KycStatuses::<T>::insert(&applicant, KycLevel::PendingReferral);
|
||||
|
||||
Self::deposit_event(Event::CitizenshipApplied { applicant, referrer, identity_hash });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Referrer approves an applicant's citizenship application
|
||||
///
|
||||
/// TRUSTLESS: Referrer takes personal responsibility for this referral
|
||||
/// ACCOUNTABILITY: If applicant turns out malicious, referrer is penalized
|
||||
///
|
||||
/// # Arguments
|
||||
/// - `applicant`: Account of the person you're vouching for
|
||||
///
|
||||
/// # Requirements
|
||||
/// - Caller must be the referrer specified in the application
|
||||
/// - Application must be in PendingReferral state
|
||||
#[pallet::call_index(1)]
|
||||
#[pallet::weight(T::WeightInfo::approve_referral())]
|
||||
pub fn approve_referral(origin: OriginFor<T>, applicant: T::AccountId) -> DispatchResult {
|
||||
let caller = ensure_signed(origin)?;
|
||||
|
||||
// Must be in PendingReferral state
|
||||
ensure!(
|
||||
KycStatuses::<T>::get(&applicant) == KycLevel::PendingReferral,
|
||||
Error::<T>::CannotApproveInCurrentState
|
||||
);
|
||||
|
||||
// Get application
|
||||
let application =
|
||||
Applications::<T>::get(&applicant).ok_or(Error::<T>::ApplicationNotFound)?;
|
||||
|
||||
// Only the referrer can approve
|
||||
ensure!(application.referrer == caller, Error::<T>::NotTheReferrer);
|
||||
|
||||
// Update status to ReferrerApproved
|
||||
KycStatuses::<T>::insert(&applicant, KycLevel::ReferrerApproved);
|
||||
|
||||
Self::deposit_event(Event::ReferralApproved { referrer: caller, applicant });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Self-confirm citizenship after referrer approval
|
||||
///
|
||||
/// TRUSTLESS: Applicant confirms themselves, no admin needed
|
||||
/// WELATI ONLY: This mints the Citizen NFT via self-confirmation
|
||||
///
|
||||
/// # Workflow
|
||||
/// 1. Deposit is returned
|
||||
/// 2. Identity hash is stored permanently
|
||||
/// 3. Status becomes Approved
|
||||
/// 4. Citizen NFT (Welati) is minted
|
||||
/// 5. Referral hooks are triggered
|
||||
#[pallet::call_index(2)]
|
||||
#[pallet::weight(T::WeightInfo::confirm_citizenship())]
|
||||
pub fn confirm_citizenship(origin: OriginFor<T>) -> DispatchResult {
|
||||
let applicant = ensure_signed(origin)?;
|
||||
|
||||
// Must be in ReferrerApproved state
|
||||
ensure!(
|
||||
KycStatuses::<T>::get(&applicant) == KycLevel::ReferrerApproved,
|
||||
Error::<T>::CannotConfirmInCurrentState
|
||||
);
|
||||
|
||||
// Get application
|
||||
let application =
|
||||
Applications::<T>::take(&applicant).ok_or(Error::<T>::ApplicationNotFound)?;
|
||||
|
||||
// Return deposit
|
||||
let deposit = T::KycApplicationDeposit::get();
|
||||
T::Currency::unreserve(&applicant, deposit);
|
||||
|
||||
// Store identity hash permanently (for proof of citizenship)
|
||||
IdentityHashes::<T>::insert(&applicant, application.identity_hash);
|
||||
|
||||
// Store referrer permanently (for direct responsibility tracking)
|
||||
// This is needed even after Applications is removed for penalty system
|
||||
CitizenReferrers::<T>::insert(&applicant, application.referrer.clone());
|
||||
|
||||
// Update status to Approved
|
||||
KycStatuses::<T>::insert(&applicant, KycLevel::Approved);
|
||||
|
||||
// Mint citizen NFT with self-confirmation (Welati tiki)
|
||||
if let Err(e) = T::CitizenNftProvider::mint_citizen_nft_confirmed(&applicant) {
|
||||
log::warn!("Failed to mint citizen NFT for {:?}: {:?}", applicant, e);
|
||||
// Don't fail - user is still a citizen
|
||||
}
|
||||
|
||||
// Trigger referral hooks (for referral pallet)
|
||||
// Pass referrer parameter to avoid data loss between pallets
|
||||
T::OnKycApproved::on_kyc_approved(&applicant, &application.referrer);
|
||||
|
||||
Self::deposit_event(Event::CitizenshipConfirmed { who: applicant });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Revoke citizenship (governance only)
|
||||
///
|
||||
/// Used for malicious actors identified by governance
|
||||
/// DIRECT RESPONSIBILITY: Triggers penalty for the referrer via referral pallet
|
||||
#[pallet::call_index(3)]
|
||||
#[pallet::weight(T::WeightInfo::revoke_citizenship())]
|
||||
pub fn revoke_citizenship(origin: OriginFor<T>, who: T::AccountId) -> DispatchResult {
|
||||
T::GovernanceOrigin::ensure_origin(origin)?;
|
||||
|
||||
ensure!(
|
||||
KycStatuses::<T>::get(&who) == KycLevel::Approved,
|
||||
Error::<T>::CannotRevokeInCurrentState
|
||||
);
|
||||
|
||||
// Update status
|
||||
KycStatuses::<T>::insert(&who, KycLevel::Revoked);
|
||||
|
||||
// Burn citizen NFT
|
||||
if let Err(e) = T::CitizenNftProvider::burn_citizen_nft(&who) {
|
||||
log::warn!("Failed to burn citizen NFT for {:?}: {:?}", who, e);
|
||||
}
|
||||
|
||||
// Trigger direct responsibility penalty for the referrer
|
||||
// This hook notifies the referral pallet to penalize the referrer
|
||||
T::OnCitizenshipRevoked::on_citizenship_revoked(&who);
|
||||
|
||||
Self::deposit_event(Event::CitizenshipRevoked { who });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Renounce citizenship (voluntary exit)
|
||||
///
|
||||
/// Users can freely leave the system
|
||||
#[pallet::call_index(4)]
|
||||
#[pallet::weight(T::WeightInfo::renounce_citizenship())]
|
||||
pub fn renounce_citizenship(origin: OriginFor<T>) -> DispatchResult {
|
||||
let who = ensure_signed(origin)?;
|
||||
|
||||
ensure!(KycStatuses::<T>::get(&who) == KycLevel::Approved, Error::<T>::NotACitizen);
|
||||
|
||||
// Burn citizen NFT
|
||||
T::CitizenNftProvider::burn_citizen_nft(&who)?;
|
||||
|
||||
// Reset status
|
||||
KycStatuses::<T>::insert(&who, KycLevel::NotStarted);
|
||||
|
||||
// Remove identity hash
|
||||
IdentityHashes::<T>::remove(&who);
|
||||
|
||||
Self::deposit_event(Event::CitizenshipRenounced { who });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Cancel pending application and retrieve deposit
|
||||
///
|
||||
/// Useful if referrer is unresponsive or user made a mistake.
|
||||
/// SAFETY: Only works in PendingReferral state (not yet approved)
|
||||
#[pallet::call_index(5)]
|
||||
#[pallet::weight(T::WeightInfo::cancel_application())]
|
||||
pub fn cancel_application(origin: OriginFor<T>) -> DispatchResult {
|
||||
let applicant = ensure_signed(origin)?;
|
||||
|
||||
// Must be in PendingReferral state (not yet approved by referrer)
|
||||
ensure!(
|
||||
KycStatuses::<T>::get(&applicant) == KycLevel::PendingReferral,
|
||||
Error::<T>::CannotCancelInCurrentState
|
||||
);
|
||||
|
||||
// Remove application
|
||||
Applications::<T>::remove(&applicant);
|
||||
|
||||
// Reset status
|
||||
KycStatuses::<T>::insert(&applicant, KycLevel::NotStarted);
|
||||
|
||||
// Unreserve deposit
|
||||
let deposit = T::KycApplicationDeposit::get();
|
||||
T::Currency::unreserve(&applicant, deposit);
|
||||
|
||||
Self::deposit_event(Event::ApplicationCancelled { who: applicant });
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============= TRAIT IMPLEMENTATIONS =============
|
||||
|
||||
pub use types::KycStatus;
|
||||
|
||||
impl<T: Config> types::KycStatus<T::AccountId> for Pallet<T> {
|
||||
fn get_kyc_status(who: &T::AccountId) -> KycLevel {
|
||||
KycStatuses::<T>::get(who)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> IdentityInfoProvider<T::AccountId, T::MaxStringLength> for Pallet<T> {
|
||||
fn get_identity_info(who: &T::AccountId) -> Option<IdentityInfo<T::MaxStringLength>> {
|
||||
// Legacy: Return from old storage if exists
|
||||
Identities::<T>::get(who)
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper methods for checking citizenship
|
||||
impl<T: Config> Pallet<T> {
|
||||
/// Check if account is a citizen
|
||||
pub fn is_citizen(who: &T::AccountId) -> bool {
|
||||
KycStatuses::<T>::get(who) == KycLevel::Approved
|
||||
}
|
||||
|
||||
/// Count total number of citizens
|
||||
pub fn citizen_count() -> u32 {
|
||||
KycStatuses::<T>::iter()
|
||||
.filter(|(_, status)| *status == KycLevel::Approved)
|
||||
.count() as u32
|
||||
}
|
||||
|
||||
/// Get the referrer of a citizen or applicant
|
||||
/// Checks both pending applications and approved citizen records
|
||||
pub fn get_referrer(who: &T::AccountId) -> Option<T::AccountId> {
|
||||
// First check permanent storage (for approved citizens)
|
||||
CitizenReferrers::<T>::get(who)
|
||||
// Then check pending applications
|
||||
.or_else(|| Applications::<T>::get(who).map(|app| app.referrer))
|
||||
}
|
||||
|
||||
/// Get identity hash of a citizen
|
||||
pub fn get_identity_hash(who: &T::AccountId) -> Option<H256> {
|
||||
IdentityHashes::<T>::get(who)
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for trust pallet integration
|
||||
pub trait CitizenshipStatusProvider<AccountId> {
|
||||
fn is_citizen(who: &AccountId) -> bool;
|
||||
}
|
||||
|
||||
impl<T: Config> CitizenshipStatusProvider<T::AccountId> for Pallet<T> {
|
||||
fn is_citizen(who: &T::AccountId) -> bool {
|
||||
KycStatuses::<T>::get(who) == KycLevel::Approved
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
use crate as pezpallet_identity_kyc;
|
||||
use pezframe_support::{
|
||||
construct_runtime, derive_impl, parameter_types,
|
||||
traits::{ConstU128, ConstU32},
|
||||
};
|
||||
use pezframe_system::EnsureRoot;
|
||||
use pezsp_core::H256;
|
||||
use pezsp_runtime::BuildStorage;
|
||||
|
||||
type Block = pezframe_system::mocking::MockBlock<Test>;
|
||||
pub type AccountId = u64;
|
||||
pub type Balance = u128;
|
||||
|
||||
// Founding citizen for genesis tests
|
||||
pub const FOUNDER: AccountId = 100;
|
||||
pub const CITIZEN_1: AccountId = 1;
|
||||
pub const CITIZEN_2: AccountId = 2;
|
||||
pub const APPLICANT: AccountId = 3;
|
||||
|
||||
construct_runtime!(
|
||||
pub enum Test
|
||||
{
|
||||
System: pezframe_system,
|
||||
Balances: pezpallet_balances,
|
||||
IdentityKyc: pezpallet_identity_kyc,
|
||||
}
|
||||
);
|
||||
|
||||
#[derive_impl(pezframe_system::config_preludes::TestDefaultConfig)]
|
||||
impl pezframe_system::Config for Test {
|
||||
type Block = Block;
|
||||
type AccountData = pezpallet_balances::AccountData<Balance>;
|
||||
}
|
||||
|
||||
#[derive_impl(pezpallet_balances::config_preludes::TestDefaultConfig)]
|
||||
impl pezpallet_balances::Config for Test {
|
||||
type Balance = Balance;
|
||||
type ExistentialDeposit = ConstU128<1>;
|
||||
type AccountStore = System;
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const KycApplicationDepositAmount: Balance = 100;
|
||||
pub const MaxStringLen: u32 = 50;
|
||||
pub const MaxCidLen: u32 = 128;
|
||||
}
|
||||
|
||||
// Mock implementation for OnKycApproved hook
|
||||
// UPDATED: Now includes referrer parameter
|
||||
pub struct MockOnKycApproved;
|
||||
impl crate::types::OnKycApproved<AccountId> for MockOnKycApproved {
|
||||
fn on_kyc_approved(_who: &AccountId, _referrer: &AccountId) {
|
||||
// No-op for tests - in real runtime this triggers referral pallet
|
||||
}
|
||||
}
|
||||
|
||||
// Mock implementation for OnCitizenshipRevoked hook
|
||||
pub struct MockOnCitizenshipRevoked;
|
||||
impl crate::types::OnCitizenshipRevoked<AccountId> for MockOnCitizenshipRevoked {
|
||||
fn on_citizenship_revoked(_who: &AccountId) {
|
||||
// No-op for tests - in real runtime this triggers penalty system
|
||||
}
|
||||
}
|
||||
|
||||
// Mock implementation for CitizenNftProvider
|
||||
pub struct MockCitizenNftProvider;
|
||||
impl crate::types::CitizenNftProvider<AccountId> for MockCitizenNftProvider {
|
||||
fn mint_citizen_nft(_who: &AccountId) -> pezsp_runtime::DispatchResult {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn mint_citizen_nft_confirmed(_who: &AccountId) -> pezsp_runtime::DispatchResult {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn burn_citizen_nft(_who: &AccountId) -> pezsp_runtime::DispatchResult {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::Config for Test {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type Currency = Balances;
|
||||
type GovernanceOrigin = EnsureRoot<Self::AccountId>;
|
||||
type WeightInfo = ();
|
||||
type OnKycApproved = MockOnKycApproved;
|
||||
type OnCitizenshipRevoked = MockOnCitizenshipRevoked;
|
||||
type CitizenNftProvider = MockCitizenNftProvider;
|
||||
type KycApplicationDeposit = KycApplicationDepositAmount;
|
||||
type MaxStringLength = MaxStringLen;
|
||||
type MaxCidLength = MaxCidLen;
|
||||
}
|
||||
|
||||
/// Build test externalities with founding citizens
|
||||
pub fn new_test_ext() -> pezsp_io::TestExternalities {
|
||||
let mut t = pezframe_system::GenesisConfig::<Test>::default().build_storage().unwrap();
|
||||
|
||||
pezpallet_balances::GenesisConfig::<Test> {
|
||||
balances: vec![
|
||||
(FOUNDER, 1_000_000),
|
||||
(CITIZEN_1, 10_000),
|
||||
(CITIZEN_2, 10_000),
|
||||
(APPLICANT, 10_000),
|
||||
],
|
||||
..Default::default()
|
||||
}
|
||||
.assimilate_storage(&mut t)
|
||||
.unwrap();
|
||||
|
||||
// Add founding citizen via genesis config
|
||||
pezpallet_identity_kyc::GenesisConfig::<Test> {
|
||||
founding_citizens: vec![
|
||||
(FOUNDER, H256::from_low_u64_be(1)), // Founder is pre-approved
|
||||
(CITIZEN_1, H256::from_low_u64_be(2)), // Citizen 1 is pre-approved
|
||||
],
|
||||
_phantom: Default::default(),
|
||||
}
|
||||
.assimilate_storage(&mut t)
|
||||
.unwrap();
|
||||
|
||||
let mut ext = pezsp_io::TestExternalities::new(t);
|
||||
ext.execute_with(|| System::set_block_number(1));
|
||||
ext
|
||||
}
|
||||
|
||||
/// Build test externalities without founding citizens (for edge case tests)
|
||||
pub fn new_test_ext_empty() -> pezsp_io::TestExternalities {
|
||||
let mut t = pezframe_system::GenesisConfig::<Test>::default().build_storage().unwrap();
|
||||
|
||||
pezpallet_balances::GenesisConfig::<Test> {
|
||||
balances: vec![(FOUNDER, 1_000_000), (CITIZEN_1, 10_000), (APPLICANT, 10_000)],
|
||||
..Default::default()
|
||||
}
|
||||
.assimilate_storage(&mut t)
|
||||
.unwrap();
|
||||
|
||||
let mut ext = pezsp_io::TestExternalities::new(t);
|
||||
ext.execute_with(|| System::set_block_number(1));
|
||||
ext
|
||||
}
|
||||
@@ -0,0 +1,551 @@
|
||||
use crate::{mock::*, types::KycLevel, Error, Event};
|
||||
use pezframe_support::{assert_noop, assert_ok, traits::Currency};
|
||||
use pezsp_core::H256;
|
||||
use pezsp_runtime::DispatchError;
|
||||
|
||||
// Kolay erişim için paletimize bir takma ad veriyoruz.
|
||||
type IdentityKycPallet = crate::Pallet<Test>;
|
||||
|
||||
// ============================================================================
|
||||
// Genesis Config Tests
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn genesis_config_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// FOUNDER and CITIZEN_1 should be pre-approved via genesis
|
||||
assert_eq!(IdentityKycPallet::kyc_status_of(FOUNDER), KycLevel::Approved);
|
||||
assert_eq!(IdentityKycPallet::kyc_status_of(CITIZEN_1), KycLevel::Approved);
|
||||
|
||||
// Their identity hashes should be stored
|
||||
assert!(IdentityKycPallet::identity_hash_of(FOUNDER).is_some());
|
||||
assert!(IdentityKycPallet::identity_hash_of(CITIZEN_1).is_some());
|
||||
|
||||
// Non-founding users should be NotStarted
|
||||
assert_eq!(IdentityKycPallet::kyc_status_of(APPLICANT), KycLevel::NotStarted);
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// apply_for_citizenship Tests
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn apply_for_citizenship_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let identity_hash = H256::from_low_u64_be(12345);
|
||||
|
||||
// APPLICANT applies with CITIZEN_1 as referrer (who is pre-approved)
|
||||
assert_ok!(IdentityKycPallet::apply_for_citizenship(
|
||||
RuntimeOrigin::signed(APPLICANT),
|
||||
identity_hash,
|
||||
CITIZEN_1
|
||||
));
|
||||
|
||||
// Check status changed to PendingReferral
|
||||
assert_eq!(IdentityKycPallet::kyc_status_of(APPLICANT), KycLevel::PendingReferral);
|
||||
|
||||
// Check application was stored
|
||||
let app = IdentityKycPallet::applications(APPLICANT).expect("Application should exist");
|
||||
assert_eq!(app.identity_hash, identity_hash);
|
||||
assert_eq!(app.referrer, CITIZEN_1);
|
||||
|
||||
// Check deposit was reserved
|
||||
assert_eq!(Balances::reserved_balance(APPLICANT), KycApplicationDepositAmount::get());
|
||||
|
||||
// Check event was emitted
|
||||
System::assert_last_event(
|
||||
Event::CitizenshipApplied { applicant: APPLICANT, referrer: CITIZEN_1, identity_hash }
|
||||
.into(),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_for_citizenship_fails_if_self_referral() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// Cannot refer yourself
|
||||
assert_noop!(
|
||||
IdentityKycPallet::apply_for_citizenship(
|
||||
RuntimeOrigin::signed(CITIZEN_1),
|
||||
H256::from_low_u64_be(999),
|
||||
CITIZEN_1 // Same as caller
|
||||
),
|
||||
Error::<Test>::SelfReferral
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_for_citizenship_fails_if_referrer_not_citizen() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// APPLICANT is not a citizen, so cannot be a referrer
|
||||
assert_noop!(
|
||||
IdentityKycPallet::apply_for_citizenship(
|
||||
RuntimeOrigin::signed(CITIZEN_2),
|
||||
H256::from_low_u64_be(999),
|
||||
APPLICANT // Not a citizen
|
||||
),
|
||||
Error::<Test>::ReferrerNotCitizen
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_for_citizenship_fails_if_already_applied() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let identity_hash = H256::from_low_u64_be(12345);
|
||||
|
||||
// First application succeeds
|
||||
assert_ok!(IdentityKycPallet::apply_for_citizenship(
|
||||
RuntimeOrigin::signed(APPLICANT),
|
||||
identity_hash,
|
||||
CITIZEN_1
|
||||
));
|
||||
|
||||
// Second application fails
|
||||
assert_noop!(
|
||||
IdentityKycPallet::apply_for_citizenship(
|
||||
RuntimeOrigin::signed(APPLICANT),
|
||||
H256::from_low_u64_be(99999),
|
||||
CITIZEN_1
|
||||
),
|
||||
Error::<Test>::ApplicationAlreadyExists
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_for_citizenship_fails_insufficient_balance() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let poor_user = 999; // No balance in genesis
|
||||
|
||||
assert_noop!(
|
||||
IdentityKycPallet::apply_for_citizenship(
|
||||
RuntimeOrigin::signed(poor_user),
|
||||
H256::from_low_u64_be(12345),
|
||||
CITIZEN_1
|
||||
),
|
||||
pezpallet_balances::Error::<Test>::InsufficientBalance
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// approve_referral Tests
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn approve_referral_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let identity_hash = H256::from_low_u64_be(12345);
|
||||
|
||||
// APPLICANT applies with CITIZEN_1 as referrer
|
||||
assert_ok!(IdentityKycPallet::apply_for_citizenship(
|
||||
RuntimeOrigin::signed(APPLICANT),
|
||||
identity_hash,
|
||||
CITIZEN_1
|
||||
));
|
||||
|
||||
// CITIZEN_1 approves the referral
|
||||
assert_ok!(IdentityKycPallet::approve_referral(
|
||||
RuntimeOrigin::signed(CITIZEN_1),
|
||||
APPLICANT
|
||||
));
|
||||
|
||||
// Check status changed to ReferrerApproved
|
||||
assert_eq!(IdentityKycPallet::kyc_status_of(APPLICANT), KycLevel::ReferrerApproved);
|
||||
|
||||
// Check event
|
||||
System::assert_last_event(
|
||||
Event::ReferralApproved { referrer: CITIZEN_1, applicant: APPLICANT }.into(),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn approve_referral_fails_if_not_referrer() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// APPLICANT applies with CITIZEN_1 as referrer
|
||||
assert_ok!(IdentityKycPallet::apply_for_citizenship(
|
||||
RuntimeOrigin::signed(APPLICANT),
|
||||
H256::from_low_u64_be(12345),
|
||||
CITIZEN_1
|
||||
));
|
||||
|
||||
// FOUNDER (different citizen) cannot approve
|
||||
assert_noop!(
|
||||
IdentityKycPallet::approve_referral(RuntimeOrigin::signed(FOUNDER), APPLICANT),
|
||||
Error::<Test>::NotTheReferrer
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn approve_referral_fails_if_not_pending() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// Try to approve referral for someone who hasn't applied
|
||||
assert_noop!(
|
||||
IdentityKycPallet::approve_referral(RuntimeOrigin::signed(CITIZEN_1), APPLICANT),
|
||||
Error::<Test>::CannotApproveInCurrentState
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// confirm_citizenship Tests (Self-confirmation for Welati NFT)
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn confirm_citizenship_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let identity_hash = H256::from_low_u64_be(12345);
|
||||
let initial_balance = Balances::free_balance(APPLICANT);
|
||||
|
||||
// Apply
|
||||
assert_ok!(IdentityKycPallet::apply_for_citizenship(
|
||||
RuntimeOrigin::signed(APPLICANT),
|
||||
identity_hash,
|
||||
CITIZEN_1
|
||||
));
|
||||
|
||||
// Referrer approves
|
||||
assert_ok!(IdentityKycPallet::approve_referral(
|
||||
RuntimeOrigin::signed(CITIZEN_1),
|
||||
APPLICANT
|
||||
));
|
||||
|
||||
// Self-confirm
|
||||
assert_ok!(IdentityKycPallet::confirm_citizenship(RuntimeOrigin::signed(APPLICANT)));
|
||||
|
||||
// Check status is Approved
|
||||
assert_eq!(IdentityKycPallet::kyc_status_of(APPLICANT), KycLevel::Approved);
|
||||
|
||||
// Check identity hash is stored permanently
|
||||
assert_eq!(IdentityKycPallet::identity_hash_of(APPLICANT), Some(identity_hash));
|
||||
|
||||
// Check referrer is stored permanently
|
||||
assert_eq!(IdentityKycPallet::citizen_referrer(APPLICANT), Some(CITIZEN_1));
|
||||
|
||||
// Check application was removed
|
||||
assert!(IdentityKycPallet::applications(APPLICANT).is_none());
|
||||
|
||||
// Check deposit was returned
|
||||
assert_eq!(Balances::reserved_balance(APPLICANT), 0);
|
||||
assert_eq!(Balances::free_balance(APPLICANT), initial_balance);
|
||||
|
||||
// Check event
|
||||
System::assert_last_event(Event::CitizenshipConfirmed { who: APPLICANT }.into());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn confirm_citizenship_fails_if_not_referrer_approved() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// Apply but don't get referrer approval
|
||||
assert_ok!(IdentityKycPallet::apply_for_citizenship(
|
||||
RuntimeOrigin::signed(APPLICANT),
|
||||
H256::from_low_u64_be(12345),
|
||||
CITIZEN_1
|
||||
));
|
||||
|
||||
// Try to self-confirm without referrer approval
|
||||
assert_noop!(
|
||||
IdentityKycPallet::confirm_citizenship(RuntimeOrigin::signed(APPLICANT)),
|
||||
Error::<Test>::CannotConfirmInCurrentState
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn confirm_citizenship_fails_if_not_applied() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// Try to confirm without applying
|
||||
assert_noop!(
|
||||
IdentityKycPallet::confirm_citizenship(RuntimeOrigin::signed(APPLICANT)),
|
||||
Error::<Test>::CannotConfirmInCurrentState
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// cancel_application Tests
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn cancel_application_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let initial_balance = Balances::free_balance(APPLICANT);
|
||||
|
||||
// Apply
|
||||
assert_ok!(IdentityKycPallet::apply_for_citizenship(
|
||||
RuntimeOrigin::signed(APPLICANT),
|
||||
H256::from_low_u64_be(12345),
|
||||
CITIZEN_1
|
||||
));
|
||||
|
||||
// Deposit should be reserved
|
||||
assert_eq!(Balances::reserved_balance(APPLICANT), KycApplicationDepositAmount::get());
|
||||
|
||||
// Cancel
|
||||
assert_ok!(IdentityKycPallet::cancel_application(RuntimeOrigin::signed(APPLICANT)));
|
||||
|
||||
// Status should be reset to NotStarted
|
||||
assert_eq!(IdentityKycPallet::kyc_status_of(APPLICANT), KycLevel::NotStarted);
|
||||
|
||||
// Application should be removed
|
||||
assert!(IdentityKycPallet::applications(APPLICANT).is_none());
|
||||
|
||||
// Deposit should be returned
|
||||
assert_eq!(Balances::reserved_balance(APPLICANT), 0);
|
||||
assert_eq!(Balances::free_balance(APPLICANT), initial_balance);
|
||||
|
||||
// Event
|
||||
System::assert_last_event(Event::ApplicationCancelled { who: APPLICANT }.into());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cancel_application_fails_if_not_pending_referral() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// Apply and get referrer approval
|
||||
assert_ok!(IdentityKycPallet::apply_for_citizenship(
|
||||
RuntimeOrigin::signed(APPLICANT),
|
||||
H256::from_low_u64_be(12345),
|
||||
CITIZEN_1
|
||||
));
|
||||
assert_ok!(IdentityKycPallet::approve_referral(
|
||||
RuntimeOrigin::signed(CITIZEN_1),
|
||||
APPLICANT
|
||||
));
|
||||
|
||||
// Cannot cancel after referrer approved (status is ReferrerApproved)
|
||||
assert_noop!(
|
||||
IdentityKycPallet::cancel_application(RuntimeOrigin::signed(APPLICANT)),
|
||||
Error::<Test>::CannotCancelInCurrentState
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cancel_application_allows_reapplication() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// First application
|
||||
assert_ok!(IdentityKycPallet::apply_for_citizenship(
|
||||
RuntimeOrigin::signed(APPLICANT),
|
||||
H256::from_low_u64_be(12345),
|
||||
CITIZEN_1
|
||||
));
|
||||
|
||||
// Cancel
|
||||
assert_ok!(IdentityKycPallet::cancel_application(RuntimeOrigin::signed(APPLICANT)));
|
||||
|
||||
// Can apply again with different referrer
|
||||
assert_ok!(IdentityKycPallet::apply_for_citizenship(
|
||||
RuntimeOrigin::signed(APPLICANT),
|
||||
H256::from_low_u64_be(99999),
|
||||
FOUNDER // Different referrer this time
|
||||
));
|
||||
|
||||
assert_eq!(IdentityKycPallet::kyc_status_of(APPLICANT), KycLevel::PendingReferral);
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// revoke_citizenship Tests (Governance action)
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn revoke_citizenship_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// Complete citizenship flow for APPLICANT
|
||||
assert_ok!(IdentityKycPallet::apply_for_citizenship(
|
||||
RuntimeOrigin::signed(APPLICANT),
|
||||
H256::from_low_u64_be(12345),
|
||||
CITIZEN_1
|
||||
));
|
||||
assert_ok!(IdentityKycPallet::approve_referral(
|
||||
RuntimeOrigin::signed(CITIZEN_1),
|
||||
APPLICANT
|
||||
));
|
||||
assert_ok!(IdentityKycPallet::confirm_citizenship(RuntimeOrigin::signed(APPLICANT)));
|
||||
|
||||
assert_eq!(IdentityKycPallet::kyc_status_of(APPLICANT), KycLevel::Approved);
|
||||
|
||||
// Governance revokes
|
||||
assert_ok!(IdentityKycPallet::revoke_citizenship(RuntimeOrigin::root(), APPLICANT));
|
||||
|
||||
// Status should be Revoked
|
||||
assert_eq!(IdentityKycPallet::kyc_status_of(APPLICANT), KycLevel::Revoked);
|
||||
|
||||
// Event
|
||||
System::assert_last_event(Event::CitizenshipRevoked { who: APPLICANT }.into());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revoke_citizenship_fails_for_bad_origin() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// Non-root cannot revoke
|
||||
assert_noop!(
|
||||
IdentityKycPallet::revoke_citizenship(RuntimeOrigin::signed(CITIZEN_1), FOUNDER),
|
||||
DispatchError::BadOrigin
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revoke_citizenship_fails_if_not_citizen() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// APPLICANT is not a citizen
|
||||
assert_noop!(
|
||||
IdentityKycPallet::revoke_citizenship(RuntimeOrigin::root(), APPLICANT),
|
||||
Error::<Test>::CannotRevokeInCurrentState
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// renounce_citizenship Tests (Voluntary exit)
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn renounce_citizenship_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// CITIZEN_1 is pre-approved, can renounce
|
||||
assert_eq!(IdentityKycPallet::kyc_status_of(CITIZEN_1), KycLevel::Approved);
|
||||
|
||||
assert_ok!(IdentityKycPallet::renounce_citizenship(RuntimeOrigin::signed(CITIZEN_1)));
|
||||
|
||||
// Status should be reset to NotStarted
|
||||
assert_eq!(IdentityKycPallet::kyc_status_of(CITIZEN_1), KycLevel::NotStarted);
|
||||
|
||||
// Identity hash should be removed
|
||||
assert!(IdentityKycPallet::identity_hash_of(CITIZEN_1).is_none());
|
||||
|
||||
// Event
|
||||
System::assert_last_event(Event::CitizenshipRenounced { who: CITIZEN_1 }.into());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn renounce_citizenship_fails_if_not_citizen() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// APPLICANT is not a citizen
|
||||
assert_noop!(
|
||||
IdentityKycPallet::renounce_citizenship(RuntimeOrigin::signed(APPLICANT)),
|
||||
Error::<Test>::NotACitizen
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Full Workflow Tests
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn full_citizenship_workflow() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let identity_hash = H256::from_low_u64_be(12345);
|
||||
|
||||
// 1. Apply
|
||||
assert_ok!(IdentityKycPallet::apply_for_citizenship(
|
||||
RuntimeOrigin::signed(APPLICANT),
|
||||
identity_hash,
|
||||
CITIZEN_1
|
||||
));
|
||||
assert_eq!(IdentityKycPallet::kyc_status_of(APPLICANT), KycLevel::PendingReferral);
|
||||
|
||||
// 2. Referrer approves
|
||||
assert_ok!(IdentityKycPallet::approve_referral(
|
||||
RuntimeOrigin::signed(CITIZEN_1),
|
||||
APPLICANT
|
||||
));
|
||||
assert_eq!(IdentityKycPallet::kyc_status_of(APPLICANT), KycLevel::ReferrerApproved);
|
||||
|
||||
// 3. Self-confirm
|
||||
assert_ok!(IdentityKycPallet::confirm_citizenship(RuntimeOrigin::signed(APPLICANT)));
|
||||
assert_eq!(IdentityKycPallet::kyc_status_of(APPLICANT), KycLevel::Approved);
|
||||
|
||||
// 4. Now APPLICANT is a citizen and can be a referrer for others
|
||||
let new_user = 50;
|
||||
// First give new_user some balance
|
||||
Balances::make_free_balance_be(&new_user, 10_000);
|
||||
|
||||
assert_ok!(IdentityKycPallet::apply_for_citizenship(
|
||||
RuntimeOrigin::signed(new_user),
|
||||
H256::from_low_u64_be(99999),
|
||||
APPLICANT // APPLICANT is now the referrer
|
||||
));
|
||||
assert_eq!(IdentityKycPallet::kyc_status_of(new_user), KycLevel::PendingReferral);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn renounce_and_reapply_workflow() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// Complete first citizenship
|
||||
assert_ok!(IdentityKycPallet::apply_for_citizenship(
|
||||
RuntimeOrigin::signed(APPLICANT),
|
||||
H256::from_low_u64_be(12345),
|
||||
CITIZEN_1
|
||||
));
|
||||
assert_ok!(IdentityKycPallet::approve_referral(
|
||||
RuntimeOrigin::signed(CITIZEN_1),
|
||||
APPLICANT
|
||||
));
|
||||
assert_ok!(IdentityKycPallet::confirm_citizenship(RuntimeOrigin::signed(APPLICANT)));
|
||||
assert_eq!(IdentityKycPallet::kyc_status_of(APPLICANT), KycLevel::Approved);
|
||||
|
||||
// Renounce
|
||||
assert_ok!(IdentityKycPallet::renounce_citizenship(RuntimeOrigin::signed(APPLICANT)));
|
||||
assert_eq!(IdentityKycPallet::kyc_status_of(APPLICANT), KycLevel::NotStarted);
|
||||
|
||||
// Can reapply (free world principle)
|
||||
assert_ok!(IdentityKycPallet::apply_for_citizenship(
|
||||
RuntimeOrigin::signed(APPLICANT),
|
||||
H256::from_low_u64_be(99999), // Different hash
|
||||
FOUNDER // Different referrer
|
||||
));
|
||||
assert_eq!(IdentityKycPallet::kyc_status_of(APPLICANT), KycLevel::PendingReferral);
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Helper Function Tests
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn is_citizen_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// Founding citizens should return true
|
||||
assert!(IdentityKycPallet::is_citizen(&FOUNDER));
|
||||
assert!(IdentityKycPallet::is_citizen(&CITIZEN_1));
|
||||
|
||||
// Non-citizens should return false
|
||||
assert!(!IdentityKycPallet::is_citizen(&APPLICANT));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_referrer_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// Complete citizenship for APPLICANT
|
||||
assert_ok!(IdentityKycPallet::apply_for_citizenship(
|
||||
RuntimeOrigin::signed(APPLICANT),
|
||||
H256::from_low_u64_be(12345),
|
||||
CITIZEN_1
|
||||
));
|
||||
assert_ok!(IdentityKycPallet::approve_referral(
|
||||
RuntimeOrigin::signed(CITIZEN_1),
|
||||
APPLICANT
|
||||
));
|
||||
assert_ok!(IdentityKycPallet::confirm_citizenship(RuntimeOrigin::signed(APPLICANT)));
|
||||
|
||||
// Should return the referrer
|
||||
assert_eq!(IdentityKycPallet::get_referrer(&APPLICANT), Some(CITIZEN_1));
|
||||
|
||||
// Founding citizens have no referrer (they were genesis)
|
||||
assert_eq!(IdentityKycPallet::get_referrer(&FOUNDER), None);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
use codec::{Decode, Encode, MaxEncodedLen};
|
||||
use pezframe_support::pezpallet_prelude::{BoundedVec, Get, RuntimeDebug};
|
||||
use scale_info::TypeInfo;
|
||||
use pezsp_core::H256;
|
||||
|
||||
/// Citizenship status levels
|
||||
/// PRIVACY: No personal data stored on-chain, only status and hash
|
||||
#[derive(
|
||||
Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen, Copy, Default,
|
||||
)]
|
||||
pub enum KycLevel {
|
||||
/// No citizenship application
|
||||
#[default]
|
||||
NotStarted,
|
||||
/// Application submitted, waiting for referrer approval
|
||||
/// TRUSTLESS: Referrer must approve before self-confirmation
|
||||
PendingReferral,
|
||||
/// Referrer approved, waiting for applicant's self-confirmation
|
||||
/// TRUSTLESS: No admin involved, applicant confirms themselves
|
||||
ReferrerApproved,
|
||||
/// Approved citizen with full rights
|
||||
Approved,
|
||||
/// Citizenship revoked (by governance or self-renounce)
|
||||
Revoked,
|
||||
}
|
||||
|
||||
/// Privacy-preserving citizenship application
|
||||
/// SECURITY: No personal data on-chain, only hash
|
||||
#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
|
||||
pub struct CitizenshipApplication<AccountId> {
|
||||
/// Hash of identity documents (actual documents stored off-chain/IPFS)
|
||||
/// Frontend calculates: H256(name + email + document_cids)
|
||||
pub identity_hash: H256,
|
||||
/// The existing citizen who vouches for this applicant
|
||||
/// TRUSTLESS: Referrer is personally responsible for their referrals
|
||||
pub referrer: AccountId,
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, Clone, Default, MaxEncodedLen)]
|
||||
pub struct IdentityInfo<MaxStringLength: Get<u32>> {
|
||||
pub name: BoundedVec<u8, MaxStringLength>,
|
||||
pub email: BoundedVec<u8, MaxStringLength>,
|
||||
}
|
||||
|
||||
// Manually implement PartialEq to avoid requiring `MaxStringLength: PartialEq`
|
||||
impl<MaxStringLength: Get<u32>> PartialEq for IdentityInfo<MaxStringLength> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.name == other.name && self.email == other.email
|
||||
}
|
||||
}
|
||||
impl<MaxStringLength: Get<u32>> Eq for IdentityInfo<MaxStringLength> {}
|
||||
|
||||
// Manually implement Debug as well for the same reason.
|
||||
impl<MaxStringLength: Get<u32>> core::fmt::Debug for IdentityInfo<MaxStringLength> {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
f.debug_struct("IdentityInfo")
|
||||
.field("name", &self.name)
|
||||
.field("email", &self.email)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<MaxStringLength: Get<u32> + 'static> TypeInfo for IdentityInfo<MaxStringLength>
|
||||
where
|
||||
BoundedVec<u8, MaxStringLength>: TypeInfo,
|
||||
{
|
||||
type Identity = Self;
|
||||
|
||||
fn type_info() -> scale_info::Type {
|
||||
scale_info::Type::builder()
|
||||
.path(scale_info::Path::new("IdentityInfo", "pezpallet_identity_kyc::types"))
|
||||
.composite(
|
||||
scale_info::build::Fields::named()
|
||||
.field(|f| {
|
||||
f.ty::<BoundedVec<u8, MaxStringLength>>()
|
||||
.name("name")
|
||||
.type_name("BoundedVec<u8, MaxStringLength>")
|
||||
})
|
||||
.field(|f| {
|
||||
f.ty::<BoundedVec<u8, MaxStringLength>>()
|
||||
.name("email")
|
||||
.type_name("BoundedVec<u8, MaxStringLength>")
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, Clone, Default, MaxEncodedLen)]
|
||||
pub struct KycApplication<MaxStringLength: Get<u32>, MaxCidLength: Get<u32>> {
|
||||
pub cids: BoundedVec<BoundedVec<u8, MaxCidLength>, MaxCidLength>,
|
||||
pub notes: BoundedVec<u8, MaxStringLength>,
|
||||
}
|
||||
|
||||
// Manually implement PartialEq to avoid requiring generic bounds to be PartialEq
|
||||
impl<MaxStringLength: Get<u32>, MaxCidLength: Get<u32>> PartialEq
|
||||
for KycApplication<MaxStringLength, MaxCidLength>
|
||||
{
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.cids == other.cids && self.notes == other.notes
|
||||
}
|
||||
}
|
||||
impl<MaxStringLength: Get<u32>, MaxCidLength: Get<u32>> Eq
|
||||
for KycApplication<MaxStringLength, MaxCidLength>
|
||||
{
|
||||
}
|
||||
|
||||
// Manually implement Debug as well for the same reason.
|
||||
impl<MaxStringLength: Get<u32>, MaxCidLength: Get<u32>> core::fmt::Debug
|
||||
for KycApplication<MaxStringLength, MaxCidLength>
|
||||
{
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
f.debug_struct("KycApplication")
|
||||
.field("cids", &self.cids)
|
||||
.field("notes", &self.notes)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<MaxStringLength: Get<u32> + 'static, MaxCidLength: Get<u32> + 'static> TypeInfo
|
||||
for KycApplication<MaxStringLength, MaxCidLength>
|
||||
where
|
||||
BoundedVec<BoundedVec<u8, MaxCidLength>, MaxCidLength>: TypeInfo,
|
||||
BoundedVec<u8, MaxStringLength>: TypeInfo,
|
||||
{
|
||||
type Identity = Self;
|
||||
|
||||
fn type_info() -> scale_info::Type {
|
||||
scale_info::Type::builder()
|
||||
.path(scale_info::Path::new("KycApplication", "pezpallet_identity_kyc::types"))
|
||||
.composite(
|
||||
scale_info::build::Fields::named()
|
||||
.field(|f| {
|
||||
f.ty::<BoundedVec<BoundedVec<u8, MaxCidLength>, MaxCidLength>>()
|
||||
.name("cids")
|
||||
.type_name("BoundedVec<BoundedVec<u8, MaxCidLength>, MaxCidLength>")
|
||||
})
|
||||
.field(|f| {
|
||||
f.ty::<BoundedVec<u8, MaxStringLength>>()
|
||||
.name("notes")
|
||||
.type_name("BoundedVec<u8, MaxStringLength>")
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
// --- Dış Dünya İçin Arayüzler (Traits) ---
|
||||
|
||||
/// Bir hesabın KYC durumunu sorgulamak için arayüz.
|
||||
pub trait KycStatus<AccountId> {
|
||||
fn get_kyc_status(who: &AccountId) -> KycLevel;
|
||||
}
|
||||
|
||||
/// Bir hesabın kimlik bilgilerini sorgulamak için arayüz.
|
||||
pub trait IdentityInfoProvider<AccountId, MaxStringLength: Get<u32>> {
|
||||
fn get_identity_info(who: &AccountId) -> Option<IdentityInfo<MaxStringLength>>;
|
||||
}
|
||||
|
||||
/// KYC onaylandığında tetiklenecek eylemleri tanımlayan arayüz.
|
||||
/// Bu trait identity-kyc palletinde tanımlanır ve diğer palletler (örn. referral)
|
||||
/// tarafından implement edilir, böylece circular dependency oluşmaz.
|
||||
///
|
||||
/// UPDATED (Gemini suggestion): Now includes referrer parameter to avoid
|
||||
/// data loss when identity-kyc and referral have separate storage.
|
||||
pub trait OnKycApproved<AccountId> {
|
||||
/// Called when a citizen is approved
|
||||
/// - `who`: The newly approved citizen
|
||||
/// - `referrer`: The citizen who vouched for them (from identity-kyc storage)
|
||||
fn on_kyc_approved(who: &AccountId, referrer: &AccountId);
|
||||
}
|
||||
|
||||
/// No-op implementation for when no hook is needed
|
||||
impl<AccountId> OnKycApproved<AccountId> for () {
|
||||
fn on_kyc_approved(_who: &AccountId, _referrer: &AccountId) {}
|
||||
}
|
||||
|
||||
/// Vatandaşlık NFT'si mintlemek için arayüz.
|
||||
/// Bu trait identity-kyc palletinde tanımlanır ve tiki pallet tarafından
|
||||
/// implement edilir, böylece circular dependency oluşmaz.
|
||||
pub trait CitizenNftProvider<AccountId> {
|
||||
fn mint_citizen_nft(who: &AccountId) -> pezsp_runtime::DispatchResult;
|
||||
|
||||
/// Mint citizen NFT with self-confirmation (uses force_mint internally)
|
||||
fn mint_citizen_nft_confirmed(who: &AccountId) -> pezsp_runtime::DispatchResult;
|
||||
|
||||
/// Burn citizen NFT when user renounces citizenship
|
||||
fn burn_citizen_nft(who: &AccountId) -> pezsp_runtime::DispatchResult;
|
||||
}
|
||||
|
||||
/// Hook called when citizenship is revoked (for direct responsibility penalty)
|
||||
/// Defined here to avoid circular dependency, implemented by referral pallet
|
||||
pub trait OnCitizenshipRevoked<AccountId> {
|
||||
fn on_citizenship_revoked(who: &AccountId);
|
||||
}
|
||||
|
||||
/// No-op implementation for when no hook is needed
|
||||
impl<AccountId> OnCitizenshipRevoked<AccountId> for () {
|
||||
fn on_citizenship_revoked(_who: &AccountId) {}
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
|
||||
//! Autogenerated weights for `pezpallet_identity_kyc`
|
||||
//!
|
||||
//! THIS FILE WAS AUTO-GENERATED USING THE BIZINIKIWI BENCHMARK CLI VERSION 32.0.0
|
||||
//! DATE: 2025-12-08, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
|
||||
//! WORST CASE MAP SIZE: `1000000`
|
||||
//! HOSTNAME: `MamostePC`, CPU: `11th Gen Intel(R) Core(TM) i9-11950H @ 2.60GHz`
|
||||
//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: `1024`
|
||||
|
||||
// Executed Command:
|
||||
// ./target/release/frame-omni-bencher
|
||||
// v1
|
||||
// benchmark
|
||||
// pallet
|
||||
// --runtime
|
||||
// target/release/wbuild/people-pezkuwichain-runtime/people_pezkuwichain_runtime.compact.compressed.wasm
|
||||
// --pallets
|
||||
// pezpallet_identity_kyc
|
||||
// -e
|
||||
// all
|
||||
// --steps
|
||||
// 50
|
||||
// --repeat
|
||||
// 20
|
||||
// --output
|
||||
// pezcumulus/teyrchains/pallets/identity-kyc/src/weights.rs
|
||||
// --template
|
||||
// bizinikiwi/.maintain/frame-weight-template.hbs
|
||||
|
||||
#![cfg_attr(rustfmt, rustfmt_skip)]
|
||||
#![allow(unused_parens)]
|
||||
#![allow(unused_imports)]
|
||||
#![allow(missing_docs)]
|
||||
#![allow(dead_code)]
|
||||
|
||||
use pezframe_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}};
|
||||
use core::marker::PhantomData;
|
||||
|
||||
/// Weight functions needed for `pezpallet_identity_kyc`.
|
||||
pub trait WeightInfo {
|
||||
fn apply_for_citizenship() -> Weight;
|
||||
fn approve_referral() -> Weight;
|
||||
fn confirm_citizenship() -> Weight;
|
||||
fn revoke_citizenship() -> Weight;
|
||||
fn renounce_citizenship() -> Weight;
|
||||
fn cancel_application() -> Weight;
|
||||
}
|
||||
|
||||
/// Weights for `pezpallet_identity_kyc` using the Bizinikiwi node and recommended hardware.
|
||||
pub struct BizinikiwiWeight<T>(PhantomData<T>);
|
||||
impl<T: pezframe_system::Config> WeightInfo for BizinikiwiWeight<T> {
|
||||
/// Storage: `IdentityKyc::KycStatuses` (r:2 w:1)
|
||||
/// Proof: `IdentityKyc::KycStatuses` (`max_values`: None, `max_size`: Some(49), added: 2524, mode: `MaxEncodedLen`)
|
||||
/// Storage: `System::Account` (r:1 w:1)
|
||||
/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
|
||||
/// Storage: `IdentityKyc::Applications` (r:0 w:1)
|
||||
/// Proof: `IdentityKyc::Applications` (`max_values`: None, `max_size`: Some(112), added: 2587, mode: `MaxEncodedLen`)
|
||||
fn apply_for_citizenship() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `253`
|
||||
// Estimated: `6038`
|
||||
// Minimum execution time: 32_436_000 picoseconds.
|
||||
Weight::from_parts(33_789_000, 6038)
|
||||
.saturating_add(T::DbWeight::get().reads(3_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(3_u64))
|
||||
}
|
||||
/// Storage: `IdentityKyc::KycStatuses` (r:1 w:1)
|
||||
/// Proof: `IdentityKyc::KycStatuses` (`max_values`: None, `max_size`: Some(49), added: 2524, mode: `MaxEncodedLen`)
|
||||
/// Storage: `IdentityKyc::Applications` (r:1 w:0)
|
||||
/// Proof: `IdentityKyc::Applications` (`max_values`: None, `max_size`: Some(112), added: 2587, mode: `MaxEncodedLen`)
|
||||
fn approve_referral() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `323`
|
||||
// Estimated: `3577`
|
||||
// Minimum execution time: 17_647_000 picoseconds.
|
||||
Weight::from_parts(18_444_000, 3577)
|
||||
.saturating_add(T::DbWeight::get().reads(2_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(1_u64))
|
||||
}
|
||||
/// Storage: `IdentityKyc::KycStatuses` (r:1 w:1)
|
||||
/// Proof: `IdentityKyc::KycStatuses` (`max_values`: None, `max_size`: Some(49), added: 2524, mode: `MaxEncodedLen`)
|
||||
/// Storage: `IdentityKyc::Applications` (r:1 w:1)
|
||||
/// Proof: `IdentityKyc::Applications` (`max_values`: None, `max_size`: Some(112), added: 2587, mode: `MaxEncodedLen`)
|
||||
/// Storage: `System::Account` (r:1 w:1)
|
||||
/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
|
||||
/// Storage: `IdentityKyc::IdentityHashes` (r:0 w:1)
|
||||
/// Proof: `IdentityKyc::IdentityHashes` (`max_values`: None, `max_size`: Some(80), added: 2555, mode: `MaxEncodedLen`)
|
||||
/// Storage: `IdentityKyc::CitizenReferrers` (r:0 w:1)
|
||||
/// Proof: `IdentityKyc::CitizenReferrers` (`max_values`: None, `max_size`: Some(80), added: 2555, mode: `MaxEncodedLen`)
|
||||
fn confirm_citizenship() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `426`
|
||||
// Estimated: `3593`
|
||||
// Minimum execution time: 37_545_000 picoseconds.
|
||||
Weight::from_parts(40_069_000, 3593)
|
||||
.saturating_add(T::DbWeight::get().reads(3_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(5_u64))
|
||||
}
|
||||
/// Storage: `IdentityKyc::KycStatuses` (r:1 w:1)
|
||||
/// Proof: `IdentityKyc::KycStatuses` (`max_values`: None, `max_size`: Some(49), added: 2524, mode: `MaxEncodedLen`)
|
||||
fn revoke_citizenship() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `150`
|
||||
// Estimated: `3514`
|
||||
// Minimum execution time: 12_919_000 picoseconds.
|
||||
Weight::from_parts(13_877_000, 3514)
|
||||
.saturating_add(T::DbWeight::get().reads(1_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(1_u64))
|
||||
}
|
||||
/// Storage: `IdentityKyc::KycStatuses` (r:1 w:1)
|
||||
/// Proof: `IdentityKyc::KycStatuses` (`max_values`: None, `max_size`: Some(49), added: 2524, mode: `MaxEncodedLen`)
|
||||
/// Storage: `IdentityKyc::IdentityHashes` (r:0 w:1)
|
||||
/// Proof: `IdentityKyc::IdentityHashes` (`max_values`: None, `max_size`: Some(80), added: 2555, mode: `MaxEncodedLen`)
|
||||
fn renounce_citizenship() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `150`
|
||||
// Estimated: `3514`
|
||||
// Minimum execution time: 14_510_000 picoseconds.
|
||||
Weight::from_parts(14_914_000, 3514)
|
||||
.saturating_add(T::DbWeight::get().reads(1_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(2_u64))
|
||||
}
|
||||
/// Storage: `IdentityKyc::KycStatuses` (r:1 w:1)
|
||||
/// Proof: `IdentityKyc::KycStatuses` (`max_values`: None, `max_size`: Some(49), added: 2524, mode: `MaxEncodedLen`)
|
||||
/// Storage: `System::Account` (r:1 w:1)
|
||||
/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
|
||||
/// Storage: `IdentityKyc::Applications` (r:0 w:1)
|
||||
/// Proof: `IdentityKyc::Applications` (`max_values`: None, `max_size`: Some(112), added: 2587, mode: `MaxEncodedLen`)
|
||||
fn cancel_application() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `299`
|
||||
// Estimated: `3593`
|
||||
// Minimum execution time: 28_146_000 picoseconds.
|
||||
Weight::from_parts(29_261_000, 3593)
|
||||
.saturating_add(T::DbWeight::get().reads(2_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(3_u64))
|
||||
}
|
||||
}
|
||||
|
||||
// For backwards compatibility and tests.
|
||||
impl WeightInfo for () {
|
||||
/// Storage: `IdentityKyc::KycStatuses` (r:2 w:1)
|
||||
/// Proof: `IdentityKyc::KycStatuses` (`max_values`: None, `max_size`: Some(49), added: 2524, mode: `MaxEncodedLen`)
|
||||
/// Storage: `System::Account` (r:1 w:1)
|
||||
/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
|
||||
/// Storage: `IdentityKyc::Applications` (r:0 w:1)
|
||||
/// Proof: `IdentityKyc::Applications` (`max_values`: None, `max_size`: Some(112), added: 2587, mode: `MaxEncodedLen`)
|
||||
fn apply_for_citizenship() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `253`
|
||||
// Estimated: `6038`
|
||||
// Minimum execution time: 32_436_000 picoseconds.
|
||||
Weight::from_parts(33_789_000, 6038)
|
||||
.saturating_add(RocksDbWeight::get().reads(3_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(3_u64))
|
||||
}
|
||||
/// Storage: `IdentityKyc::KycStatuses` (r:1 w:1)
|
||||
/// Proof: `IdentityKyc::KycStatuses` (`max_values`: None, `max_size`: Some(49), added: 2524, mode: `MaxEncodedLen`)
|
||||
/// Storage: `IdentityKyc::Applications` (r:1 w:0)
|
||||
/// Proof: `IdentityKyc::Applications` (`max_values`: None, `max_size`: Some(112), added: 2587, mode: `MaxEncodedLen`)
|
||||
fn approve_referral() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `323`
|
||||
// Estimated: `3577`
|
||||
// Minimum execution time: 17_647_000 picoseconds.
|
||||
Weight::from_parts(18_444_000, 3577)
|
||||
.saturating_add(RocksDbWeight::get().reads(2_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(1_u64))
|
||||
}
|
||||
/// Storage: `IdentityKyc::KycStatuses` (r:1 w:1)
|
||||
/// Proof: `IdentityKyc::KycStatuses` (`max_values`: None, `max_size`: Some(49), added: 2524, mode: `MaxEncodedLen`)
|
||||
/// Storage: `IdentityKyc::Applications` (r:1 w:1)
|
||||
/// Proof: `IdentityKyc::Applications` (`max_values`: None, `max_size`: Some(112), added: 2587, mode: `MaxEncodedLen`)
|
||||
/// Storage: `System::Account` (r:1 w:1)
|
||||
/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
|
||||
/// Storage: `IdentityKyc::IdentityHashes` (r:0 w:1)
|
||||
/// Proof: `IdentityKyc::IdentityHashes` (`max_values`: None, `max_size`: Some(80), added: 2555, mode: `MaxEncodedLen`)
|
||||
/// Storage: `IdentityKyc::CitizenReferrers` (r:0 w:1)
|
||||
/// Proof: `IdentityKyc::CitizenReferrers` (`max_values`: None, `max_size`: Some(80), added: 2555, mode: `MaxEncodedLen`)
|
||||
fn confirm_citizenship() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `426`
|
||||
// Estimated: `3593`
|
||||
// Minimum execution time: 37_545_000 picoseconds.
|
||||
Weight::from_parts(40_069_000, 3593)
|
||||
.saturating_add(RocksDbWeight::get().reads(3_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(5_u64))
|
||||
}
|
||||
/// Storage: `IdentityKyc::KycStatuses` (r:1 w:1)
|
||||
/// Proof: `IdentityKyc::KycStatuses` (`max_values`: None, `max_size`: Some(49), added: 2524, mode: `MaxEncodedLen`)
|
||||
fn revoke_citizenship() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `150`
|
||||
// Estimated: `3514`
|
||||
// Minimum execution time: 12_919_000 picoseconds.
|
||||
Weight::from_parts(13_877_000, 3514)
|
||||
.saturating_add(RocksDbWeight::get().reads(1_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(1_u64))
|
||||
}
|
||||
/// Storage: `IdentityKyc::KycStatuses` (r:1 w:1)
|
||||
/// Proof: `IdentityKyc::KycStatuses` (`max_values`: None, `max_size`: Some(49), added: 2524, mode: `MaxEncodedLen`)
|
||||
/// Storage: `IdentityKyc::IdentityHashes` (r:0 w:1)
|
||||
/// Proof: `IdentityKyc::IdentityHashes` (`max_values`: None, `max_size`: Some(80), added: 2555, mode: `MaxEncodedLen`)
|
||||
fn renounce_citizenship() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `150`
|
||||
// Estimated: `3514`
|
||||
// Minimum execution time: 14_510_000 picoseconds.
|
||||
Weight::from_parts(14_914_000, 3514)
|
||||
.saturating_add(RocksDbWeight::get().reads(1_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(2_u64))
|
||||
}
|
||||
/// Storage: `IdentityKyc::KycStatuses` (r:1 w:1)
|
||||
/// Proof: `IdentityKyc::KycStatuses` (`max_values`: None, `max_size`: Some(49), added: 2524, mode: `MaxEncodedLen`)
|
||||
/// Storage: `System::Account` (r:1 w:1)
|
||||
/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
|
||||
/// Storage: `IdentityKyc::Applications` (r:0 w:1)
|
||||
/// Proof: `IdentityKyc::Applications` (`max_values`: None, `max_size`: Some(112), added: 2587, mode: `MaxEncodedLen`)
|
||||
fn cancel_application() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `299`
|
||||
// Estimated: `3593`
|
||||
// Minimum execution time: 28_146_000 picoseconds.
|
||||
Weight::from_parts(29_261_000, 3593)
|
||||
.saturating_add(RocksDbWeight::get().reads(2_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(3_u64))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
[package]
|
||||
name = "pezpallet-perwerde"
|
||||
version = "1.0.0"
|
||||
description = "PezkuwiChain Egitim (Education) Management Pallet"
|
||||
authors.workspace = true
|
||||
homepage.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
publish = false
|
||||
repository.workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
||||
[dependencies]
|
||||
codec = { workspace = true, default-features = false, features = ["derive"] }
|
||||
scale-info = { default-features = false, features = [
|
||||
"derive",
|
||||
], workspace = true }
|
||||
serde = { version = "1.0", default-features = false, features = [
|
||||
"derive",
|
||||
], optional = true }
|
||||
|
||||
pezframe-benchmarking = { optional = true, workspace = true }
|
||||
pezframe-support = { default-features = false, workspace = true }
|
||||
pezframe-system = { default-features = false, workspace = true }
|
||||
log = { default-features = false, workspace = true }
|
||||
pezsp-runtime = { default-features = false, workspace = true }
|
||||
pezsp-std = { default-features = false, workspace = true }
|
||||
|
||||
# PezkuwiChain'in özel tiplerini ve trait'lerini içeren kütüphane
|
||||
pezkuwi-primitives = { workspace = true, default-features = false }
|
||||
|
||||
# Mock ve benchmark için gerekli olanlar buraya optional olarak eklendi
|
||||
pezpallet-balances = { workspace = true, default-features = false, optional = true }
|
||||
pezpallet-collective = { workspace = true, default-features = false, optional = true }
|
||||
pezsp-core = { workspace = true, default-features = false, optional = true }
|
||||
pezsp-io = { workspace = true, default-features = false, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
# pezpallet-sudo = { workspace = true, default-features = false }
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = [
|
||||
"codec/std",
|
||||
"pezframe-benchmarking?/std",
|
||||
"pezframe-support/std",
|
||||
"pezframe-system/std",
|
||||
"log/std",
|
||||
"pezpallet-balances/std",
|
||||
"pezpallet-collective/std",
|
||||
"pezkuwi-primitives/std",
|
||||
"scale-info/std",
|
||||
"serde",
|
||||
"serde?/std",
|
||||
"pezsp-core/std",
|
||||
"pezsp-io/std",
|
||||
"pezsp-runtime/std",
|
||||
"pezsp-std/std",
|
||||
]
|
||||
runtime-benchmarks = [
|
||||
"pezframe-benchmarking/runtime-benchmarks",
|
||||
"pezframe-support/runtime-benchmarks",
|
||||
"pezframe-system/runtime-benchmarks",
|
||||
"pezpallet-balances",
|
||||
"pezpallet-balances?/runtime-benchmarks",
|
||||
"pezpallet-collective",
|
||||
"pezpallet-collective?/runtime-benchmarks",
|
||||
"pezkuwi-primitives/runtime-benchmarks",
|
||||
"pezsp-core",
|
||||
"pezsp-io",
|
||||
"pezsp-io?/runtime-benchmarks",
|
||||
"pezsp-runtime/runtime-benchmarks",
|
||||
]
|
||||
try-runtime = [
|
||||
"pezframe-support/try-runtime",
|
||||
"pezframe-system/try-runtime",
|
||||
"pezpallet-balances?/try-runtime",
|
||||
"pezpallet-collective?/try-runtime",
|
||||
"pezsp-runtime/try-runtime",
|
||||
]
|
||||
@@ -0,0 +1,114 @@
|
||||
//! Benchmarking setup for pezpallet-perwerde
|
||||
#![cfg(feature = "runtime-benchmarks")]
|
||||
|
||||
use super::{Pallet as Perwerde, *};
|
||||
use pezframe_benchmarking::v2::*;
|
||||
use pezframe_support::{pezpallet_prelude::Get, BoundedVec};
|
||||
use pezframe_system::RawOrigin;
|
||||
extern crate alloc;
|
||||
use alloc::vec;
|
||||
|
||||
const SEED: u32 = 0;
|
||||
|
||||
// Helper function to create BoundedVec in benchmarks
|
||||
fn create_bounded_vec<L: Get<u32>>(s: &[u8]) -> BoundedVec<u8, L> {
|
||||
s.to_vec().try_into().unwrap()
|
||||
}
|
||||
|
||||
#[benchmarks]
|
||||
mod benchmarks {
|
||||
use super::*;
|
||||
|
||||
#[benchmark]
|
||||
fn create_course() {
|
||||
let name: BoundedVec<u8, T::MaxCourseNameLength> =
|
||||
create_bounded_vec(b"Bizinikiwi training");
|
||||
let description: BoundedVec<u8, T::MaxCourseDescLength> =
|
||||
create_bounded_vec(b"This training covers Bizinikiwi basics.");
|
||||
let content_link: BoundedVec<u8, T::MaxCourseLinkLength> =
|
||||
create_bounded_vec(b"http://example.com");
|
||||
|
||||
// In benchmark environment, AdminOrigin is bypassed
|
||||
// We use Root origin which will satisfy the origin check
|
||||
|
||||
#[extrinsic_call]
|
||||
create_course(RawOrigin::Root, name.clone(), description.clone(), content_link.clone());
|
||||
|
||||
assert!(Courses::<T>::get(0).is_some());
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn enroll() {
|
||||
let student: T::AccountId = whitelisted_caller();
|
||||
let course_id = 0;
|
||||
|
||||
// Setup: Create a course first using root
|
||||
Perwerde::<T>::create_course(
|
||||
RawOrigin::Root.into(),
|
||||
create_bounded_vec(b"Benchmark Course"),
|
||||
create_bounded_vec(b"Description"),
|
||||
create_bounded_vec(b"Link"),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
#[extrinsic_call]
|
||||
enroll(RawOrigin::Signed(student.clone()), course_id);
|
||||
|
||||
assert!(Enrollments::<T>::get((student, course_id)).is_some());
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn complete_course() {
|
||||
let student: T::AccountId = whitelisted_caller();
|
||||
let course_id = 0;
|
||||
let points = 10;
|
||||
|
||||
// Setup: Create course and enroll student
|
||||
// Root creates the course via AdminOrigin
|
||||
Perwerde::<T>::create_course(
|
||||
RawOrigin::Root.into(),
|
||||
create_bounded_vec(b"Benchmark Course"),
|
||||
create_bounded_vec(b"Description"),
|
||||
create_bounded_vec(b"Link"),
|
||||
)
|
||||
.unwrap();
|
||||
Perwerde::<T>::enroll(RawOrigin::Signed(student.clone()).into(), course_id).unwrap();
|
||||
|
||||
// Get the actual owner from the created course
|
||||
let course = Courses::<T>::get(course_id).unwrap();
|
||||
let owner = course.owner;
|
||||
|
||||
// complete_course requires the owner to sign, not root
|
||||
#[extrinsic_call]
|
||||
complete_course(RawOrigin::Signed(owner), student.clone(), course_id, points);
|
||||
|
||||
let enrollment = Enrollments::<T>::get((student, course_id)).unwrap();
|
||||
assert!(enrollment.completed_at.is_some());
|
||||
assert_eq!(enrollment.points_earned, points);
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn archive_course() {
|
||||
let course_id = 0;
|
||||
|
||||
// Setup: Create course first
|
||||
Perwerde::<T>::create_course(
|
||||
RawOrigin::Root.into(),
|
||||
create_bounded_vec(b"Benchmark Course"),
|
||||
create_bounded_vec(b"Description"),
|
||||
create_bounded_vec(b"Link"),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// archive_course requires AdminOrigin (which is Root in our config)
|
||||
// The AdminOrigin::try_origin for Root returns the admin account (Alice)
|
||||
// which matches the course owner from create_course
|
||||
#[extrinsic_call]
|
||||
archive_course(RawOrigin::Root, course_id);
|
||||
|
||||
let course = Courses::<T>::get(course_id).unwrap();
|
||||
assert_eq!(course.status, CourseStatus::Archived);
|
||||
}
|
||||
|
||||
impl_benchmark_test_suite!(Perwerde, crate::mock::new_test_ext(), crate::mock::Test);
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
//! # Perwerde (Education) Pallet
|
||||
//!
|
||||
//! A pallet for managing educational courses, student enrollments, and achievement tracking.
|
||||
//!
|
||||
//! ## Overview
|
||||
//!
|
||||
//! The Perwerde pallet implements an on-chain educational platform where:
|
||||
//! - Educators create and manage courses with IPFS-linked content
|
||||
//! - Students enroll in courses and track their progress
|
||||
//! - Course completion earns points that contribute to trust scores
|
||||
//! - Educational achievements are permanently recorded on-chain
|
||||
//!
|
||||
//! ## Core Features
|
||||
//!
|
||||
//! ### Course Management
|
||||
//! - Admins create courses with name, description, and content links (IPFS)
|
||||
//! - Courses can be active or archived
|
||||
//! - Each course has a unique ID and owner
|
||||
//! - Course metadata is immutable after creation
|
||||
//!
|
||||
//! ### Student Enrollment
|
||||
//! - Students enroll in active courses
|
||||
//! - One enrollment per student per course
|
||||
//! - Enrollment history tracked with block numbers
|
||||
//! - Students can be enrolled in multiple courses simultaneously
|
||||
//!
|
||||
//! ### Completion & Points
|
||||
//! - Course owners mark student completions
|
||||
//! - Points awarded upon completion
|
||||
//! - Points contribute to Perwerde score for trust calculation
|
||||
//! - Completion timestamps recorded permanently
|
||||
//!
|
||||
//! ## Perwerde Score System
|
||||
//!
|
||||
//! The Perwerde score is derived from total education points:
|
||||
//! - Each completed course awards points
|
||||
//! - Points accumulate over time
|
||||
//! - Score used by `pezpallet-trust` for composite trust calculation
|
||||
//! - Higher education achievement improves ecosystem standing
|
||||
//!
|
||||
//! ## Interface
|
||||
//!
|
||||
//! ### Extrinsics
|
||||
//!
|
||||
//! - `create_course(name, description, content_link)` - Create new educational course (admin)
|
||||
//! - `enroll_student(course_id)` - Enroll in an active course (user)
|
||||
//! - `mark_course_completed(student, course_id, points)` - Award completion points (course owner)
|
||||
//! - `archive_course(course_id)` - Archive a course (course owner)
|
||||
//!
|
||||
//! ### Storage
|
||||
//!
|
||||
//! - `Courses` - Course metadata indexed by course ID
|
||||
//! - `NextCourseId` - Auto-incrementing course ID counter
|
||||
//! - `Enrollments` - Student enrollment records (student, course_id) → Enrollment
|
||||
//! - `StudentCourses` - Per-student list of enrolled course IDs
|
||||
//!
|
||||
//! ### Integration
|
||||
//!
|
||||
//! - Implements `PerwerdeScoreProvider` trait for `pezpallet-trust`
|
||||
//! - Education scores contribute to validator eligibility
|
||||
//! - Course completion history visible to governance
|
||||
//!
|
||||
//! ## Security Features
|
||||
//!
|
||||
//! - Only course owners can mark completions
|
||||
//! - Active courses required for enrollment
|
||||
//! - No duplicate enrollments
|
||||
//! - Maximum courses per student limit
|
||||
//! - Admin-only course creation
|
||||
//!
|
||||
//! ## Runtime Integration Example
|
||||
//!
|
||||
//! ```ignore
|
||||
//! impl pezpallet_perwerde::Config for Runtime {
|
||||
//! type RuntimeEvent = RuntimeEvent;
|
||||
//! type AdminOrigin = EnsureRoot<AccountId>;
|
||||
//! type WeightInfo = pezpallet_perwerde::weights::BizinikiwiWeight<Runtime>;
|
||||
//! type MaxCourseNameLength = ConstU32<128>;
|
||||
//! type MaxCourseDescLength = ConstU32<512>;
|
||||
//! type MaxCourseLinkLength = ConstU32<256>;
|
||||
//! type MaxStudentsPerCourse = ConstU32<100>;
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
pub use pallet::*;
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
mod benchmarking;
|
||||
pub mod weights;
|
||||
|
||||
// These modules should only be compiled in `std` environment.
|
||||
#[cfg(all(feature = "std", any(test, feature = "runtime-benchmarks")))]
|
||||
pub mod mock;
|
||||
|
||||
#[cfg(all(feature = "std", test))]
|
||||
mod tests;
|
||||
|
||||
pub use weights::WeightInfo;
|
||||
|
||||
#[pezframe_support::pallet]
|
||||
pub mod pallet {
|
||||
use super::*;
|
||||
use pezframe_support::{
|
||||
dispatch::DispatchResult,
|
||||
pezpallet_prelude::*,
|
||||
traits::{EnsureOrigin, Get},
|
||||
};
|
||||
use pezframe_system::pezpallet_prelude::*;
|
||||
|
||||
#[pallet::pallet]
|
||||
pub struct Pallet<T>(_);
|
||||
|
||||
#[pallet::config]
|
||||
pub trait Config: pezframe_system::Config {
|
||||
type RuntimeEvent: From<Event<Self>> + IsType<<Self as pezframe_system::Config>::RuntimeEvent>;
|
||||
type AdminOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = Self::AccountId>;
|
||||
type WeightInfo: WeightInfo;
|
||||
|
||||
#[pallet::constant]
|
||||
type MaxCourseNameLength: Get<u32>;
|
||||
#[pallet::constant]
|
||||
type MaxCourseDescLength: Get<u32>;
|
||||
#[pallet::constant]
|
||||
type MaxCourseLinkLength: Get<u32>;
|
||||
#[pallet::constant]
|
||||
type MaxStudentsPerCourse: Get<u32>;
|
||||
|
||||
/// Maximum number of courses a single student can enroll in
|
||||
/// Used for StudentCourses storage bound
|
||||
#[pallet::constant]
|
||||
type MaxCoursesPerStudent: Get<u32>;
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
|
||||
pub enum CourseStatus {
|
||||
Active,
|
||||
Archived,
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
|
||||
#[scale_info(skip_type_params(T))]
|
||||
pub struct Course<T: Config> {
|
||||
pub id: u32,
|
||||
pub owner: T::AccountId,
|
||||
pub name: BoundedVec<u8, T::MaxCourseNameLength>,
|
||||
pub description: BoundedVec<u8, T::MaxCourseDescLength>,
|
||||
pub content_link: BoundedVec<u8, T::MaxCourseLinkLength>,
|
||||
pub status: CourseStatus,
|
||||
pub created_at: BlockNumberFor<T>,
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
|
||||
#[scale_info(skip_type_params(T))]
|
||||
pub struct Enrollment<T: Config> {
|
||||
pub student: T::AccountId,
|
||||
pub course_id: u32,
|
||||
pub enrolled_at: BlockNumberFor<T>,
|
||||
pub completed_at: Option<BlockNumberFor<T>>,
|
||||
pub points_earned: u32,
|
||||
}
|
||||
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn courses)]
|
||||
pub type Courses<T: Config> = StorageMap<_, Blake2_128Concat, u32, Course<T>, OptionQuery>;
|
||||
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn next_course_id)]
|
||||
pub type NextCourseId<T: Config> = StorageValue<_, u32, ValueQuery>;
|
||||
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn enrollments)]
|
||||
pub type Enrollments<T: Config> =
|
||||
StorageMap<_, Blake2_128Concat, (T::AccountId, u32), Enrollment<T>, OptionQuery>;
|
||||
|
||||
/// Per-student list of enrolled course IDs
|
||||
/// UPDATED (Gemini suggestion): Uses MaxCoursesPerStudent instead of MaxStudentsPerCourse
|
||||
/// This is the correct semantic - limits how many courses ONE student can take
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn student_courses)]
|
||||
pub type StudentCourses<T: Config> = StorageMap<
|
||||
_,
|
||||
Blake2_128Concat,
|
||||
T::AccountId,
|
||||
BoundedVec<u32, T::MaxCoursesPerStudent>,
|
||||
ValueQuery,
|
||||
>;
|
||||
|
||||
#[pallet::event]
|
||||
#[pallet::generate_deposit(pub(super) fn deposit_event)]
|
||||
pub enum Event<T: Config> {
|
||||
CourseCreated { course_id: u32, owner: T::AccountId },
|
||||
StudentEnrolled { student: T::AccountId, course_id: u32 },
|
||||
CourseCompleted { student: T::AccountId, course_id: u32, points: u32 },
|
||||
CourseArchived { course_id: u32 },
|
||||
}
|
||||
|
||||
#[pallet::error]
|
||||
pub enum Error<T> {
|
||||
CourseNotFound,
|
||||
AlreadyEnrolled,
|
||||
NotEnrolled,
|
||||
CourseNotActive,
|
||||
CourseAlreadyCompleted,
|
||||
NotCourseOwner,
|
||||
TooManyCourses,
|
||||
}
|
||||
|
||||
#[pallet::call]
|
||||
impl<T: Config> Pallet<T> {
|
||||
#[pallet::call_index(0)]
|
||||
#[pallet::weight(T::WeightInfo::create_course())]
|
||||
pub fn create_course(
|
||||
origin: OriginFor<T>,
|
||||
name: BoundedVec<u8, T::MaxCourseNameLength>,
|
||||
description: BoundedVec<u8, T::MaxCourseDescLength>,
|
||||
content_link: BoundedVec<u8, T::MaxCourseLinkLength>,
|
||||
) -> DispatchResult {
|
||||
let owner = T::AdminOrigin::ensure_origin(origin)?;
|
||||
let course_id = NextCourseId::<T>::get();
|
||||
|
||||
// Parameters are already bounded, no conversion needed
|
||||
let course = Course {
|
||||
id: course_id,
|
||||
owner: owner.clone(),
|
||||
name,
|
||||
description,
|
||||
content_link,
|
||||
status: CourseStatus::Active,
|
||||
created_at: pezframe_system::Pallet::<T>::block_number(),
|
||||
};
|
||||
|
||||
Courses::<T>::insert(course_id, course);
|
||||
NextCourseId::<T>::mutate(|id| *id += 1);
|
||||
|
||||
Self::deposit_event(Event::CourseCreated { course_id, owner });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[pallet::call_index(1)]
|
||||
#[pallet::weight(T::WeightInfo::enroll())]
|
||||
pub fn enroll(origin: OriginFor<T>, course_id: u32) -> DispatchResult {
|
||||
let student = ensure_signed(origin)?;
|
||||
let course = Courses::<T>::get(course_id).ok_or(Error::<T>::CourseNotFound)?;
|
||||
ensure!(course.status == CourseStatus::Active, Error::<T>::CourseNotActive);
|
||||
ensure!(
|
||||
!Enrollments::<T>::contains_key((&student, course_id)),
|
||||
Error::<T>::AlreadyEnrolled
|
||||
);
|
||||
|
||||
let enrollment = Enrollment {
|
||||
student: student.clone(),
|
||||
course_id,
|
||||
enrolled_at: pezframe_system::Pallet::<T>::block_number(),
|
||||
completed_at: None,
|
||||
points_earned: 0,
|
||||
};
|
||||
|
||||
Enrollments::<T>::insert((&student, course_id), enrollment);
|
||||
StudentCourses::<T>::try_mutate(&student, |courses| {
|
||||
courses.try_push(course_id).map_err(|_| Error::<T>::TooManyCourses)
|
||||
})?;
|
||||
|
||||
Self::deposit_event(Event::StudentEnrolled { student, course_id });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Mark a student's course as completed and award points
|
||||
/// SECURITY: Only the course owner can mark completions, not students themselves
|
||||
#[pallet::call_index(2)]
|
||||
#[pallet::weight(T::WeightInfo::complete_course())]
|
||||
pub fn complete_course(
|
||||
origin: OriginFor<T>,
|
||||
student: T::AccountId,
|
||||
course_id: u32,
|
||||
points: u32,
|
||||
) -> DispatchResult {
|
||||
let caller = ensure_signed(origin)?;
|
||||
|
||||
// Verify caller is the course owner
|
||||
let course = Courses::<T>::get(course_id).ok_or(Error::<T>::CourseNotFound)?;
|
||||
ensure!(course.owner == caller, Error::<T>::NotCourseOwner);
|
||||
|
||||
// Get and validate enrollment
|
||||
let mut enrollment =
|
||||
Enrollments::<T>::get((&student, course_id)).ok_or(Error::<T>::NotEnrolled)?;
|
||||
ensure!(enrollment.completed_at.is_none(), Error::<T>::CourseAlreadyCompleted);
|
||||
|
||||
// Mark completion
|
||||
enrollment.completed_at = Some(pezframe_system::Pallet::<T>::block_number());
|
||||
enrollment.points_earned = points;
|
||||
|
||||
Enrollments::<T>::insert((&student, course_id), enrollment);
|
||||
|
||||
Self::deposit_event(Event::CourseCompleted { student, course_id, points });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[pallet::call_index(3)]
|
||||
#[pallet::weight(T::WeightInfo::archive_course())]
|
||||
pub fn archive_course(origin: OriginFor<T>, course_id: u32) -> DispatchResult {
|
||||
let caller = T::AdminOrigin::ensure_origin(origin)?;
|
||||
let mut course = Courses::<T>::get(course_id).ok_or(Error::<T>::CourseNotFound)?;
|
||||
ensure!(course.owner == caller, Error::<T>::NotCourseOwner);
|
||||
|
||||
course.status = CourseStatus::Archived;
|
||||
Courses::<T>::insert(course_id, course);
|
||||
|
||||
Self::deposit_event(Event::CourseArchived { course_id });
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> Pallet<T> {
|
||||
pub fn get_perwerde_score(who: &T::AccountId) -> u32 {
|
||||
StudentCourses::<T>::get(who)
|
||||
.iter()
|
||||
.filter_map(|course_id| Enrollments::<T>::get((who, *course_id)))
|
||||
.filter(|enrollment| enrollment.completed_at.is_some())
|
||||
.map(|enrollment| enrollment.points_earned)
|
||||
.sum()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
use crate as pezpallet_perwerde;
|
||||
use pezframe_support::{
|
||||
construct_runtime, parameter_types,
|
||||
traits::{ConstU128, ConstU16, ConstU32, ConstU64, Everything, SortedMembers},
|
||||
};
|
||||
use pezframe_system::EnsureRoot;
|
||||
use pezsp_core::H256;
|
||||
use pezsp_runtime::{
|
||||
traits::{BlakeTwo256, IdentityLookup},
|
||||
BuildStorage,
|
||||
};
|
||||
|
||||
// Temel tipleri tanımlıyoruz.
|
||||
pub type AccountId = u64;
|
||||
pub type Balance = u128;
|
||||
pub type BlockNumber = u64;
|
||||
pub type Block = pezframe_system::mocking::MockBlock<Test>;
|
||||
|
||||
// Test runtime'ımızı kuruyoruz.
|
||||
construct_runtime!(
|
||||
pub enum Test
|
||||
{
|
||||
System: pezframe_system,
|
||||
Balances: pezpallet_balances,
|
||||
Perwerde: pezpallet_perwerde,
|
||||
Council: pezpallet_collective::<Instance1>,
|
||||
}
|
||||
);
|
||||
|
||||
// pezframe_system için implementasyon.
|
||||
impl pezframe_system::Config for Test {
|
||||
type BaseCallFilter = Everything;
|
||||
type BlockWeights = ();
|
||||
type BlockLength = ();
|
||||
type DbWeight = ();
|
||||
type RuntimeOrigin = RuntimeOrigin;
|
||||
type RuntimeCall = RuntimeCall;
|
||||
type Nonce = u64;
|
||||
type Hash = H256;
|
||||
type Hashing = BlakeTwo256;
|
||||
type AccountId = AccountId;
|
||||
type Lookup = IdentityLookup<Self::AccountId>;
|
||||
type Block = Block;
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type BlockHashCount = ConstU64<250>;
|
||||
type Version = ();
|
||||
type PalletInfo = PalletInfo;
|
||||
type AccountData = pezpallet_balances::AccountData<Balance>;
|
||||
type OnNewAccount = ();
|
||||
type OnKilledAccount = ();
|
||||
type SystemWeightInfo = ();
|
||||
type SS58Prefix = ConstU16<42>;
|
||||
type OnSetCode = ();
|
||||
type MaxConsumers = ConstU32<16>;
|
||||
type RuntimeTask = ();
|
||||
type ExtensionsWeightInfo = ();
|
||||
type SingleBlockMigrations = ();
|
||||
type MultiBlockMigrator = ();
|
||||
type PreInherents = ();
|
||||
type PostInherents = ();
|
||||
type PostTransactions = ();
|
||||
}
|
||||
|
||||
// pezpallet_balances için implementasyon.
|
||||
impl pezpallet_balances::Config for Test {
|
||||
type Balance = Balance;
|
||||
type DustRemoval = ();
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type ExistentialDeposit = ConstU128<1>;
|
||||
type AccountStore = System;
|
||||
type WeightInfo = ();
|
||||
type MaxLocks = ConstU32<50>;
|
||||
type MaxReserves = ();
|
||||
type ReserveIdentifier = [u8; 8];
|
||||
type FreezeIdentifier = ();
|
||||
type MaxFreezes = ConstU32<1>;
|
||||
type RuntimeHoldReason = ();
|
||||
type RuntimeFreezeReason = ();
|
||||
type DoneSlashHandler = ();
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const MaxCourseNameLength: u32 = 100;
|
||||
pub const MaxCourseDescLength: u32 = 500;
|
||||
pub const MaxCourseLinkLength: u32 = 200;
|
||||
pub const MaxStudentsPerCourse: u32 = 100; // Reduced for test performance
|
||||
pub const MaxCoursesPerStudent: u32 = 50; // Max courses a student can enroll in
|
||||
}
|
||||
|
||||
// --- KESİN ÇÖZÜM BURADA BAŞLIYOR ---
|
||||
|
||||
// AdminOrigin'i test etmek için kendi özel yetki sağlayıcımızı oluşturuyoruz.
|
||||
use pezframe_system::EnsureSignedBy;
|
||||
|
||||
// Bu struct, derleyicinin talep ettiği `SortedMembers` trait'ini manuel olarak uygular.
|
||||
// Bu, harici ve versiyona bağımlı araçlara olan ihtiyacı ortadan kaldırır.
|
||||
pub struct TestAdminProvider;
|
||||
impl SortedMembers<AccountId> for TestAdminProvider {
|
||||
fn sorted_members() -> Vec<AccountId> {
|
||||
// Test için admin olarak sadece 0 ID'li hesabı yetkili kılıyoruz.
|
||||
vec![0]
|
||||
}
|
||||
}
|
||||
|
||||
impl pezpallet_perwerde::Config for Test {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
// AdminOrigin'i, kendi yazdığımız ve sadece 0'ı admin kabul eden sağlayıcıya bağlıyoruz.
|
||||
type AdminOrigin = EnsureSignedBy<TestAdminProvider, AccountId>;
|
||||
type WeightInfo = ();
|
||||
type MaxCourseNameLength = MaxCourseNameLength;
|
||||
type MaxCourseDescLength = MaxCourseDescLength;
|
||||
type MaxCourseLinkLength = MaxCourseLinkLength;
|
||||
type MaxStudentsPerCourse = MaxStudentsPerCourse;
|
||||
type MaxCoursesPerStudent = MaxCoursesPerStudent;
|
||||
}
|
||||
|
||||
// Council Paletinin Mock Kurulumu (construct_runtime'da gerekli olduğu için kalıyor)
|
||||
use pezpallet_collective::Instance1;
|
||||
parameter_types! {
|
||||
pub const CouncilMotionDuration: BlockNumber = 5 * 60; // 5 minutes
|
||||
pub const CouncilMaxProposals: u32 = 100;
|
||||
pub const CouncilMaxMembers: u32 = 100;
|
||||
pub MaxProposalWeight: pezframe_support::weights::Weight = pezframe_support::weights::Weight::from_parts(1_000_000_000, 0);
|
||||
}
|
||||
impl pezpallet_collective::Config<Instance1> for Test {
|
||||
type RuntimeOrigin = RuntimeOrigin;
|
||||
type Proposal = RuntimeCall;
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type MotionDuration = CouncilMotionDuration;
|
||||
type MaxProposals = CouncilMaxProposals;
|
||||
type MaxMembers = CouncilMaxMembers;
|
||||
type DefaultVote = pezpallet_collective::PrimeDefaultVote;
|
||||
type WeightInfo = ();
|
||||
type SetMembersOrigin = EnsureRoot<AccountId>;
|
||||
type MaxProposalWeight = MaxProposalWeight;
|
||||
type DisapproveOrigin = EnsureRoot<AccountId>;
|
||||
type KillOrigin = EnsureRoot<AccountId>;
|
||||
type Consideration = ();
|
||||
}
|
||||
|
||||
pub fn new_test_ext() -> pezsp_io::TestExternalities {
|
||||
let t = pezframe_system::GenesisConfig::<Test>::default().build_storage().unwrap();
|
||||
// `pezpallet-collective`'in genesis'ini de kurmamıza gerek kalmadı çünkü artık testimiz ona bağlı
|
||||
// değil.
|
||||
let mut ext = pezsp_io::TestExternalities::new(t);
|
||||
ext.execute_with(|| System::set_block_number(1));
|
||||
ext
|
||||
}
|
||||
@@ -0,0 +1,621 @@
|
||||
use crate::{
|
||||
mock::{new_test_ext, Perwerde as PerwerdePallet, RuntimeOrigin, System, Test},
|
||||
Event,
|
||||
};
|
||||
use pezframe_support::{assert_noop, assert_ok, pezpallet_prelude::Get, BoundedVec};
|
||||
use pezsp_runtime::DispatchError;
|
||||
|
||||
fn create_bounded_vec<L: Get<u32>>(s: &[u8]) -> BoundedVec<u8, L> {
|
||||
s.to_vec().try_into().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_course_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// Admin olarak mock.rs'te TestAdminProvider içinde tanımladığımız hesabı kullanıyoruz.
|
||||
let admin_account_id = 0;
|
||||
|
||||
// Eylem: Yetkili admin ile kurs oluştur.
|
||||
assert_ok!(PerwerdePallet::create_course(
|
||||
RuntimeOrigin::signed(admin_account_id),
|
||||
create_bounded_vec(b"Blockchain 101"),
|
||||
create_bounded_vec(b"Giris seviyesi"),
|
||||
create_bounded_vec(b"http://example.com")
|
||||
));
|
||||
|
||||
// Doğrulama
|
||||
assert!(crate::Courses::<Test>::contains_key(0));
|
||||
let course = crate::Courses::<Test>::get(0).unwrap();
|
||||
assert_eq!(course.owner, admin_account_id);
|
||||
System::assert_last_event(
|
||||
Event::CourseCreated { course_id: 0, owner: admin_account_id }.into(),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_course_fails_for_non_admin() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// Admin (0) dışındaki bir hesap (2) kurs oluşturamaz.
|
||||
let non_admin = 2;
|
||||
assert_noop!(
|
||||
PerwerdePallet::create_course(
|
||||
RuntimeOrigin::signed(non_admin),
|
||||
create_bounded_vec(b"Hacking 101"),
|
||||
create_bounded_vec(b"Yetkisiz kurs"),
|
||||
create_bounded_vec(b"http://example.com")
|
||||
),
|
||||
DispatchError::BadOrigin
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ENROLL TESTS (8 tests)
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn enroll_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let admin = 0;
|
||||
let student = 1;
|
||||
|
||||
// Create course first
|
||||
assert_ok!(PerwerdePallet::create_course(
|
||||
RuntimeOrigin::signed(admin),
|
||||
create_bounded_vec(b"Rust Basics"),
|
||||
create_bounded_vec(b"Learn Rust"),
|
||||
create_bounded_vec(b"http://example.com")
|
||||
));
|
||||
|
||||
// Student enrolls
|
||||
assert_ok!(PerwerdePallet::enroll(RuntimeOrigin::signed(student), 0));
|
||||
|
||||
// Verify enrollment
|
||||
let enrollment = crate::Enrollments::<Test>::get((student, 0)).unwrap();
|
||||
assert_eq!(enrollment.student, student);
|
||||
assert_eq!(enrollment.course_id, 0);
|
||||
assert_eq!(enrollment.completed_at, None);
|
||||
assert_eq!(enrollment.points_earned, 0);
|
||||
|
||||
// Verify StudentCourses updated
|
||||
let student_courses = crate::StudentCourses::<Test>::get(student);
|
||||
assert!(student_courses.contains(&0));
|
||||
|
||||
System::assert_last_event(Event::StudentEnrolled { student, course_id: 0 }.into());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enroll_fails_for_nonexistent_course() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let student = 1;
|
||||
assert_noop!(
|
||||
PerwerdePallet::enroll(RuntimeOrigin::signed(student), 999),
|
||||
crate::Error::<Test>::CourseNotFound
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enroll_fails_for_archived_course() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let admin = 0;
|
||||
let student = 1;
|
||||
|
||||
// Create and archive course
|
||||
assert_ok!(PerwerdePallet::create_course(
|
||||
RuntimeOrigin::signed(admin),
|
||||
create_bounded_vec(b"Old Course"),
|
||||
create_bounded_vec(b"Archived"),
|
||||
create_bounded_vec(b"http://example.com")
|
||||
));
|
||||
assert_ok!(PerwerdePallet::archive_course(RuntimeOrigin::signed(admin), 0));
|
||||
|
||||
// Try to enroll in archived course
|
||||
assert_noop!(
|
||||
PerwerdePallet::enroll(RuntimeOrigin::signed(student), 0),
|
||||
crate::Error::<Test>::CourseNotActive
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enroll_fails_if_already_enrolled() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let admin = 0;
|
||||
let student = 1;
|
||||
|
||||
// Create course
|
||||
assert_ok!(PerwerdePallet::create_course(
|
||||
RuntimeOrigin::signed(admin),
|
||||
create_bounded_vec(b"Course"),
|
||||
create_bounded_vec(b"Description"),
|
||||
create_bounded_vec(b"http://example.com")
|
||||
));
|
||||
|
||||
// First enrollment succeeds
|
||||
assert_ok!(PerwerdePallet::enroll(RuntimeOrigin::signed(student), 0));
|
||||
|
||||
// Second enrollment fails
|
||||
assert_noop!(
|
||||
PerwerdePallet::enroll(RuntimeOrigin::signed(student), 0),
|
||||
crate::Error::<Test>::AlreadyEnrolled
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_students_can_enroll_same_course() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let admin = 0;
|
||||
let student1 = 1;
|
||||
let student2 = 2;
|
||||
let student3 = 3;
|
||||
|
||||
// Create course
|
||||
assert_ok!(PerwerdePallet::create_course(
|
||||
RuntimeOrigin::signed(admin),
|
||||
create_bounded_vec(b"Popular Course"),
|
||||
create_bounded_vec(b"Many students"),
|
||||
create_bounded_vec(b"http://example.com")
|
||||
));
|
||||
|
||||
// Multiple students enroll
|
||||
assert_ok!(PerwerdePallet::enroll(RuntimeOrigin::signed(student1), 0));
|
||||
assert_ok!(PerwerdePallet::enroll(RuntimeOrigin::signed(student2), 0));
|
||||
assert_ok!(PerwerdePallet::enroll(RuntimeOrigin::signed(student3), 0));
|
||||
|
||||
// Verify all enrollments
|
||||
assert!(crate::Enrollments::<Test>::contains_key((student1, 0)));
|
||||
assert!(crate::Enrollments::<Test>::contains_key((student2, 0)));
|
||||
assert!(crate::Enrollments::<Test>::contains_key((student3, 0)));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn student_can_enroll_multiple_courses() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let admin = 0;
|
||||
let student = 1;
|
||||
|
||||
// Create 3 courses
|
||||
for i in 0..3 {
|
||||
assert_ok!(PerwerdePallet::create_course(
|
||||
RuntimeOrigin::signed(admin),
|
||||
create_bounded_vec(format!("Course {}", i).as_bytes()),
|
||||
create_bounded_vec(b"Description"),
|
||||
create_bounded_vec(b"http://example.com")
|
||||
));
|
||||
}
|
||||
|
||||
// Student enrolls in all 3
|
||||
assert_ok!(PerwerdePallet::enroll(RuntimeOrigin::signed(student), 0));
|
||||
assert_ok!(PerwerdePallet::enroll(RuntimeOrigin::signed(student), 1));
|
||||
assert_ok!(PerwerdePallet::enroll(RuntimeOrigin::signed(student), 2));
|
||||
|
||||
// Verify StudentCourses
|
||||
let student_courses = crate::StudentCourses::<Test>::get(student);
|
||||
assert_eq!(student_courses.len(), 3);
|
||||
assert!(student_courses.contains(&0));
|
||||
assert!(student_courses.contains(&1));
|
||||
assert!(student_courses.contains(&2));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enroll_fails_when_too_many_courses() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let admin = 0;
|
||||
let student = 1;
|
||||
|
||||
// MaxCoursesPerStudent is 50, so create and enroll in 50 courses
|
||||
for i in 0..50 {
|
||||
assert_ok!(PerwerdePallet::create_course(
|
||||
RuntimeOrigin::signed(admin),
|
||||
create_bounded_vec(format!("Course {}", i).as_bytes()),
|
||||
create_bounded_vec(b"Desc"),
|
||||
create_bounded_vec(b"http://example.com")
|
||||
));
|
||||
assert_ok!(PerwerdePallet::enroll(RuntimeOrigin::signed(student), i));
|
||||
}
|
||||
|
||||
// Create one more course
|
||||
assert_ok!(PerwerdePallet::create_course(
|
||||
RuntimeOrigin::signed(admin),
|
||||
create_bounded_vec(b"Course 50"),
|
||||
create_bounded_vec(b"Desc"),
|
||||
create_bounded_vec(b"http://example.com")
|
||||
));
|
||||
|
||||
// Enrollment should fail - student already enrolled in max courses
|
||||
assert_noop!(
|
||||
PerwerdePallet::enroll(RuntimeOrigin::signed(student), 50),
|
||||
crate::Error::<Test>::TooManyCourses
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enroll_event_emitted_correctly() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let admin = 0;
|
||||
let student = 5;
|
||||
|
||||
assert_ok!(PerwerdePallet::create_course(
|
||||
RuntimeOrigin::signed(admin),
|
||||
create_bounded_vec(b"Test"),
|
||||
create_bounded_vec(b"Test"),
|
||||
create_bounded_vec(b"http://test.com")
|
||||
));
|
||||
|
||||
assert_ok!(PerwerdePallet::enroll(RuntimeOrigin::signed(student), 0));
|
||||
|
||||
System::assert_last_event(Event::StudentEnrolled { student: 5, course_id: 0 }.into());
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// COMPLETE_COURSE TESTS (8 tests)
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn complete_course_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let admin = 0;
|
||||
let student = 1;
|
||||
let points = 95;
|
||||
|
||||
// Setup: Create course and enroll
|
||||
assert_ok!(PerwerdePallet::create_course(
|
||||
RuntimeOrigin::signed(admin),
|
||||
create_bounded_vec(b"Course"),
|
||||
create_bounded_vec(b"Desc"),
|
||||
create_bounded_vec(b"http://example.com")
|
||||
));
|
||||
assert_ok!(PerwerdePallet::enroll(RuntimeOrigin::signed(student), 0));
|
||||
|
||||
// Complete the course (course owner completes for student)
|
||||
assert_ok!(PerwerdePallet::complete_course(
|
||||
RuntimeOrigin::signed(admin),
|
||||
student,
|
||||
0,
|
||||
points
|
||||
));
|
||||
|
||||
// Verify completion
|
||||
let enrollment = crate::Enrollments::<Test>::get((student, 0)).unwrap();
|
||||
assert!(enrollment.completed_at.is_some());
|
||||
assert_eq!(enrollment.points_earned, points);
|
||||
|
||||
System::assert_last_event(Event::CourseCompleted { student, course_id: 0, points }.into());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complete_course_fails_without_enrollment() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let admin = 0;
|
||||
let student = 1;
|
||||
|
||||
// Create course but don't enroll
|
||||
assert_ok!(PerwerdePallet::create_course(
|
||||
RuntimeOrigin::signed(admin),
|
||||
create_bounded_vec(b"Course"),
|
||||
create_bounded_vec(b"Desc"),
|
||||
create_bounded_vec(b"http://example.com")
|
||||
));
|
||||
|
||||
// Try to complete without enrollment (admin tries to complete for non-enrolled student)
|
||||
assert_noop!(
|
||||
PerwerdePallet::complete_course(RuntimeOrigin::signed(admin), student, 0, 100),
|
||||
crate::Error::<Test>::NotEnrolled
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complete_course_fails_if_already_completed() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let admin = 0;
|
||||
let student = 1;
|
||||
|
||||
// Setup
|
||||
assert_ok!(PerwerdePallet::create_course(
|
||||
RuntimeOrigin::signed(admin),
|
||||
create_bounded_vec(b"Course"),
|
||||
create_bounded_vec(b"Desc"),
|
||||
create_bounded_vec(b"http://example.com")
|
||||
));
|
||||
assert_ok!(PerwerdePallet::enroll(RuntimeOrigin::signed(student), 0));
|
||||
|
||||
// First completion succeeds (admin completes)
|
||||
assert_ok!(PerwerdePallet::complete_course(RuntimeOrigin::signed(admin), student, 0, 85));
|
||||
|
||||
// Second completion fails
|
||||
assert_noop!(
|
||||
PerwerdePallet::complete_course(RuntimeOrigin::signed(admin), student, 0, 90),
|
||||
crate::Error::<Test>::CourseAlreadyCompleted
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complete_course_with_zero_points() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let admin = 0;
|
||||
let student = 1;
|
||||
|
||||
assert_ok!(PerwerdePallet::create_course(
|
||||
RuntimeOrigin::signed(admin),
|
||||
create_bounded_vec(b"Course"),
|
||||
create_bounded_vec(b"Desc"),
|
||||
create_bounded_vec(b"http://example.com")
|
||||
));
|
||||
assert_ok!(PerwerdePallet::enroll(RuntimeOrigin::signed(student), 0));
|
||||
|
||||
// Complete with 0 points (failed course)
|
||||
assert_ok!(PerwerdePallet::complete_course(RuntimeOrigin::signed(admin), student, 0, 0));
|
||||
|
||||
let enrollment = crate::Enrollments::<Test>::get((student, 0)).unwrap();
|
||||
assert_eq!(enrollment.points_earned, 0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complete_course_with_max_points() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let admin = 0;
|
||||
let student = 1;
|
||||
|
||||
assert_ok!(PerwerdePallet::create_course(
|
||||
RuntimeOrigin::signed(admin),
|
||||
create_bounded_vec(b"Course"),
|
||||
create_bounded_vec(b"Desc"),
|
||||
create_bounded_vec(b"http://example.com")
|
||||
));
|
||||
assert_ok!(PerwerdePallet::enroll(RuntimeOrigin::signed(student), 0));
|
||||
|
||||
// Complete with maximum points
|
||||
assert_ok!(PerwerdePallet::complete_course(
|
||||
RuntimeOrigin::signed(admin),
|
||||
student,
|
||||
0,
|
||||
u32::MAX
|
||||
));
|
||||
|
||||
let enrollment = crate::Enrollments::<Test>::get((student, 0)).unwrap();
|
||||
assert_eq!(enrollment.points_earned, u32::MAX);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_students_complete_same_course() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let admin = 0;
|
||||
|
||||
assert_ok!(PerwerdePallet::create_course(
|
||||
RuntimeOrigin::signed(admin),
|
||||
create_bounded_vec(b"Course"),
|
||||
create_bounded_vec(b"Desc"),
|
||||
create_bounded_vec(b"http://example.com")
|
||||
));
|
||||
|
||||
// 3 students enroll and admin completes with different scores
|
||||
for i in 1u64..=3 {
|
||||
assert_ok!(PerwerdePallet::enroll(RuntimeOrigin::signed(i), 0));
|
||||
assert_ok!(PerwerdePallet::complete_course(
|
||||
RuntimeOrigin::signed(admin),
|
||||
i,
|
||||
0,
|
||||
(70 + (i * 10)) as u32
|
||||
));
|
||||
}
|
||||
|
||||
// Verify each completion
|
||||
for i in 1u64..=3 {
|
||||
let enrollment = crate::Enrollments::<Test>::get((i, 0)).unwrap();
|
||||
assert!(enrollment.completed_at.is_some());
|
||||
assert_eq!(enrollment.points_earned, (70 + (i * 10)) as u32);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn student_completes_multiple_courses() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let admin = 0;
|
||||
let student = 1;
|
||||
|
||||
// Create 3 courses
|
||||
for i in 0..3 {
|
||||
assert_ok!(PerwerdePallet::create_course(
|
||||
RuntimeOrigin::signed(admin),
|
||||
create_bounded_vec(format!("Course {}", i).as_bytes()),
|
||||
create_bounded_vec(b"Desc"),
|
||||
create_bounded_vec(b"http://example.com")
|
||||
));
|
||||
assert_ok!(PerwerdePallet::enroll(RuntimeOrigin::signed(student), i));
|
||||
}
|
||||
|
||||
// Complete all 3 (admin completes for student)
|
||||
assert_ok!(PerwerdePallet::complete_course(RuntimeOrigin::signed(admin), student, 0, 80));
|
||||
assert_ok!(PerwerdePallet::complete_course(RuntimeOrigin::signed(admin), student, 1, 90));
|
||||
assert_ok!(PerwerdePallet::complete_course(RuntimeOrigin::signed(admin), student, 2, 95));
|
||||
|
||||
// Verify all completions
|
||||
for i in 0..3 {
|
||||
let enrollment = crate::Enrollments::<Test>::get((student, i)).unwrap();
|
||||
assert!(enrollment.completed_at.is_some());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complete_course_event_emitted() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let admin = 0;
|
||||
let student = 7;
|
||||
let points = 88;
|
||||
|
||||
assert_ok!(PerwerdePallet::create_course(
|
||||
RuntimeOrigin::signed(admin),
|
||||
create_bounded_vec(b"Test"),
|
||||
create_bounded_vec(b"Test"),
|
||||
create_bounded_vec(b"http://test.com")
|
||||
));
|
||||
assert_ok!(PerwerdePallet::enroll(RuntimeOrigin::signed(student), 0));
|
||||
assert_ok!(PerwerdePallet::complete_course(
|
||||
RuntimeOrigin::signed(admin),
|
||||
student,
|
||||
0,
|
||||
points
|
||||
));
|
||||
|
||||
System::assert_last_event(
|
||||
Event::CourseCompleted { student: 7, course_id: 0, points: 88 }.into(),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ARCHIVE_COURSE TESTS (4 tests)
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn archive_course_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let admin = 0;
|
||||
|
||||
assert_ok!(PerwerdePallet::create_course(
|
||||
RuntimeOrigin::signed(admin),
|
||||
create_bounded_vec(b"Course"),
|
||||
create_bounded_vec(b"Desc"),
|
||||
create_bounded_vec(b"http://example.com")
|
||||
));
|
||||
|
||||
assert_ok!(PerwerdePallet::archive_course(RuntimeOrigin::signed(admin), 0));
|
||||
|
||||
let course = crate::Courses::<Test>::get(0).unwrap();
|
||||
assert_eq!(course.status, crate::CourseStatus::Archived);
|
||||
|
||||
System::assert_last_event(Event::CourseArchived { course_id: 0 }.into());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn archive_course_fails_for_non_owner() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let admin = 0;
|
||||
let other_user = 1;
|
||||
|
||||
assert_ok!(PerwerdePallet::create_course(
|
||||
RuntimeOrigin::signed(admin),
|
||||
create_bounded_vec(b"Course"),
|
||||
create_bounded_vec(b"Desc"),
|
||||
create_bounded_vec(b"http://example.com")
|
||||
));
|
||||
|
||||
// Non-owner cannot archive
|
||||
assert_noop!(
|
||||
PerwerdePallet::archive_course(RuntimeOrigin::signed(other_user), 0),
|
||||
DispatchError::BadOrigin
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn archive_course_fails_for_nonexistent_course() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let admin = 0;
|
||||
|
||||
assert_noop!(
|
||||
PerwerdePallet::archive_course(RuntimeOrigin::signed(admin), 999),
|
||||
crate::Error::<Test>::CourseNotFound
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn archived_course_cannot_accept_new_enrollments() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let admin = 0;
|
||||
let student = 1;
|
||||
|
||||
// Create and archive
|
||||
assert_ok!(PerwerdePallet::create_course(
|
||||
RuntimeOrigin::signed(admin),
|
||||
create_bounded_vec(b"Course"),
|
||||
create_bounded_vec(b"Desc"),
|
||||
create_bounded_vec(b"http://example.com")
|
||||
));
|
||||
assert_ok!(PerwerdePallet::archive_course(RuntimeOrigin::signed(admin), 0));
|
||||
|
||||
// Try to enroll - should fail
|
||||
assert_noop!(
|
||||
PerwerdePallet::enroll(RuntimeOrigin::signed(student), 0),
|
||||
crate::Error::<Test>::CourseNotActive
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// INTEGRATION & STORAGE TESTS (2 tests)
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn storage_consistency_check() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let admin = 0;
|
||||
let student = 1;
|
||||
|
||||
// Create course
|
||||
assert_ok!(PerwerdePallet::create_course(
|
||||
RuntimeOrigin::signed(admin),
|
||||
create_bounded_vec(b"Course"),
|
||||
create_bounded_vec(b"Desc"),
|
||||
create_bounded_vec(b"http://example.com")
|
||||
));
|
||||
|
||||
// Enroll
|
||||
assert_ok!(PerwerdePallet::enroll(RuntimeOrigin::signed(student), 0));
|
||||
|
||||
// Check storage consistency
|
||||
assert!(crate::Courses::<Test>::contains_key(0));
|
||||
assert!(crate::Enrollments::<Test>::contains_key((student, 0)));
|
||||
|
||||
let student_courses = crate::StudentCourses::<Test>::get(student);
|
||||
assert_eq!(student_courses.len(), 1);
|
||||
assert!(student_courses.contains(&0));
|
||||
|
||||
let enrollment = crate::Enrollments::<Test>::get((student, 0)).unwrap();
|
||||
assert_eq!(enrollment.course_id, 0);
|
||||
assert_eq!(enrollment.student, student);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn next_course_id_increments_correctly() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let admin = 0;
|
||||
|
||||
assert_eq!(crate::NextCourseId::<Test>::get(), 0);
|
||||
|
||||
// Create 5 courses
|
||||
for i in 0..5 {
|
||||
assert_ok!(PerwerdePallet::create_course(
|
||||
RuntimeOrigin::signed(admin),
|
||||
create_bounded_vec(format!("Course {}", i).as_bytes()),
|
||||
create_bounded_vec(b"Desc"),
|
||||
create_bounded_vec(b"http://example.com")
|
||||
));
|
||||
|
||||
assert_eq!(crate::NextCourseId::<Test>::get(), i + 1);
|
||||
}
|
||||
|
||||
// Verify all courses exist
|
||||
for i in 0..5 {
|
||||
assert!(crate::Courses::<Test>::contains_key(i));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
|
||||
//! Autogenerated weights for `pezpallet_perwerde`
|
||||
//!
|
||||
//! THIS FILE WAS AUTO-GENERATED USING THE BIZINIKIWI BENCHMARK CLI VERSION 32.0.0
|
||||
//! DATE: 2025-12-08, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
|
||||
//! WORST CASE MAP SIZE: `1000000`
|
||||
//! HOSTNAME: `MamostePC`, CPU: `11th Gen Intel(R) Core(TM) i9-11950H @ 2.60GHz`
|
||||
//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: `1024`
|
||||
|
||||
// Executed Command:
|
||||
// ./target/release/frame-omni-bencher
|
||||
// v1
|
||||
// benchmark
|
||||
// pallet
|
||||
// --runtime
|
||||
// target/release/wbuild/people-pezkuwichain-runtime/people_pezkuwichain_runtime.compact.compressed.wasm
|
||||
// --pallets
|
||||
// pezpallet_perwerde
|
||||
// -e
|
||||
// all
|
||||
// --steps
|
||||
// 50
|
||||
// --repeat
|
||||
// 20
|
||||
// --output
|
||||
// pezcumulus/teyrchains/pallets/perwerde/src/weights.rs
|
||||
// --template
|
||||
// bizinikiwi/.maintain/frame-weight-template.hbs
|
||||
|
||||
#![cfg_attr(rustfmt, rustfmt_skip)]
|
||||
#![allow(unused_parens)]
|
||||
#![allow(unused_imports)]
|
||||
#![allow(missing_docs)]
|
||||
#![allow(dead_code)]
|
||||
|
||||
use pezframe_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}};
|
||||
use core::marker::PhantomData;
|
||||
|
||||
/// Weight functions needed for `pezpallet_perwerde`.
|
||||
pub trait WeightInfo {
|
||||
fn create_course() -> Weight;
|
||||
fn enroll() -> Weight;
|
||||
fn complete_course() -> Weight;
|
||||
fn archive_course() -> Weight;
|
||||
}
|
||||
|
||||
/// Weights for `pezpallet_perwerde` using the Bizinikiwi node and recommended hardware.
|
||||
pub struct BizinikiwiWeight<T>(PhantomData<T>);
|
||||
impl<T: pezframe_system::Config> WeightInfo for BizinikiwiWeight<T> {
|
||||
/// Storage: `Perwerde::NextCourseId` (r:1 w:1)
|
||||
/// Proof: `Perwerde::NextCourseId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Perwerde::Courses` (r:0 w:1)
|
||||
/// Proof: `Perwerde::Courses` (`max_values`: None, `max_size`: Some(963), added: 3438, mode: `MaxEncodedLen`)
|
||||
fn create_course() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `3`
|
||||
// Estimated: `1489`
|
||||
// Minimum execution time: 21_032_000 picoseconds.
|
||||
Weight::from_parts(22_777_000, 1489)
|
||||
.saturating_add(T::DbWeight::get().reads(1_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(2_u64))
|
||||
}
|
||||
/// Storage: `Perwerde::Courses` (r:1 w:0)
|
||||
/// Proof: `Perwerde::Courses` (`max_values`: None, `max_size`: Some(963), added: 3438, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Perwerde::Enrollments` (r:1 w:1)
|
||||
/// Proof: `Perwerde::Enrollments` (`max_values`: None, `max_size`: Some(101), added: 2576, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Perwerde::StudentCourses` (r:1 w:1)
|
||||
/// Proof: `Perwerde::StudentCourses` (`max_values`: None, `max_size`: Some(249), added: 2724, mode: `MaxEncodedLen`)
|
||||
fn enroll() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `149`
|
||||
// Estimated: `4428`
|
||||
// Minimum execution time: 33_652_000 picoseconds.
|
||||
Weight::from_parts(37_083_000, 4428)
|
||||
.saturating_add(T::DbWeight::get().reads(3_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(2_u64))
|
||||
}
|
||||
/// Storage: `Perwerde::Courses` (r:1 w:0)
|
||||
/// Proof: `Perwerde::Courses` (`max_values`: None, `max_size`: Some(963), added: 3438, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Perwerde::Enrollments` (r:1 w:1)
|
||||
/// Proof: `Perwerde::Enrollments` (`max_values`: None, `max_size`: Some(101), added: 2576, mode: `MaxEncodedLen`)
|
||||
fn complete_course() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `307`
|
||||
// Estimated: `4428`
|
||||
// Minimum execution time: 33_123_000 picoseconds.
|
||||
Weight::from_parts(37_458_000, 4428)
|
||||
.saturating_add(T::DbWeight::get().reads(2_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(1_u64))
|
||||
}
|
||||
/// Storage: `Perwerde::Courses` (r:1 w:1)
|
||||
/// Proof: `Perwerde::Courses` (`max_values`: None, `max_size`: Some(963), added: 3438, mode: `MaxEncodedLen`)
|
||||
fn archive_course() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `149`
|
||||
// Estimated: `4428`
|
||||
// Minimum execution time: 24_529_000 picoseconds.
|
||||
Weight::from_parts(27_529_000, 4428)
|
||||
.saturating_add(T::DbWeight::get().reads(1_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(1_u64))
|
||||
}
|
||||
}
|
||||
|
||||
// For backwards compatibility and tests.
|
||||
impl WeightInfo for () {
|
||||
/// Storage: `Perwerde::NextCourseId` (r:1 w:1)
|
||||
/// Proof: `Perwerde::NextCourseId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Perwerde::Courses` (r:0 w:1)
|
||||
/// Proof: `Perwerde::Courses` (`max_values`: None, `max_size`: Some(963), added: 3438, mode: `MaxEncodedLen`)
|
||||
fn create_course() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `3`
|
||||
// Estimated: `1489`
|
||||
// Minimum execution time: 21_032_000 picoseconds.
|
||||
Weight::from_parts(22_777_000, 1489)
|
||||
.saturating_add(RocksDbWeight::get().reads(1_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(2_u64))
|
||||
}
|
||||
/// Storage: `Perwerde::Courses` (r:1 w:0)
|
||||
/// Proof: `Perwerde::Courses` (`max_values`: None, `max_size`: Some(963), added: 3438, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Perwerde::Enrollments` (r:1 w:1)
|
||||
/// Proof: `Perwerde::Enrollments` (`max_values`: None, `max_size`: Some(101), added: 2576, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Perwerde::StudentCourses` (r:1 w:1)
|
||||
/// Proof: `Perwerde::StudentCourses` (`max_values`: None, `max_size`: Some(249), added: 2724, mode: `MaxEncodedLen`)
|
||||
fn enroll() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `149`
|
||||
// Estimated: `4428`
|
||||
// Minimum execution time: 33_652_000 picoseconds.
|
||||
Weight::from_parts(37_083_000, 4428)
|
||||
.saturating_add(RocksDbWeight::get().reads(3_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(2_u64))
|
||||
}
|
||||
/// Storage: `Perwerde::Courses` (r:1 w:0)
|
||||
/// Proof: `Perwerde::Courses` (`max_values`: None, `max_size`: Some(963), added: 3438, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Perwerde::Enrollments` (r:1 w:1)
|
||||
/// Proof: `Perwerde::Enrollments` (`max_values`: None, `max_size`: Some(101), added: 2576, mode: `MaxEncodedLen`)
|
||||
fn complete_course() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `307`
|
||||
// Estimated: `4428`
|
||||
// Minimum execution time: 33_123_000 picoseconds.
|
||||
Weight::from_parts(37_458_000, 4428)
|
||||
.saturating_add(RocksDbWeight::get().reads(2_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(1_u64))
|
||||
}
|
||||
/// Storage: `Perwerde::Courses` (r:1 w:1)
|
||||
/// Proof: `Perwerde::Courses` (`max_values`: None, `max_size`: Some(963), added: 3438, mode: `MaxEncodedLen`)
|
||||
fn archive_course() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `149`
|
||||
// Estimated: `4428`
|
||||
// Minimum execution time: 24_529_000 picoseconds.
|
||||
Weight::from_parts(27_529_000, 4428)
|
||||
.saturating_add(RocksDbWeight::get().reads(1_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(1_u64))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
[package]
|
||||
name = "pezpallet-pez-rewards"
|
||||
version = "1.0.0"
|
||||
description = "PezkuwiChain Trust Score Based Rewards Distribution System"
|
||||
authors.workspace = true
|
||||
homepage.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
publish = false
|
||||
repository.workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
||||
[dependencies]
|
||||
codec = { workspace = true, default-features = false, features = ["derive"] }
|
||||
scale-info = { default-features = false, features = [
|
||||
"derive",
|
||||
], workspace = true }
|
||||
serde = { version = "1.0", default-features = false, features = [
|
||||
"derive",
|
||||
], optional = true }
|
||||
|
||||
pezframe-support = { default-features = false, workspace = true }
|
||||
pezframe-system = { default-features = false, workspace = true }
|
||||
log = { default-features = false, workspace = true }
|
||||
pezsp-runtime = { default-features = false, workspace = true }
|
||||
pezsp-std = { default-features = false, workspace = true }
|
||||
|
||||
# PezkuwiChain'in özel tiplerini ve trait'lerini içeren kütüphane
|
||||
pezkuwi-primitives = { workspace = true, default-features = false }
|
||||
|
||||
# Standart Bizinikiwi paletleri
|
||||
pezpallet-balances = { default-features = false, workspace = true }
|
||||
pezpallet-nfts = { default-features = false, workspace = true }
|
||||
pezpallet-scheduler = { default-features = false, workspace = true }
|
||||
|
||||
# PezkuwiChain özel paletleri
|
||||
pezpallet-pez-treasury = { workspace = true, default-features = false }
|
||||
pezpallet-trust = { workspace = true, default-features = false }
|
||||
|
||||
# Test ve Benchmark için Gerekli İsteğe Bağlı Bağımlılıklar
|
||||
pezframe-benchmarking = { optional = true, workspace = true }
|
||||
pezsp-core = { workspace = true, default-features = false, optional = true }
|
||||
pezsp-io = { workspace = true, default-features = false, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
# Test için gerekli olan bağımlılıklar
|
||||
pezframe-system = { workspace = true, default-features = false, features = [
|
||||
"std",
|
||||
] }
|
||||
pezpallet-assets = { workspace = true, default-features = false, features = [
|
||||
"std",
|
||||
] }
|
||||
pezpallet-balances = { workspace = true, default-features = false, features = [
|
||||
"std",
|
||||
] }
|
||||
pezpallet-identity-kyc = { workspace = true, default-features = false, features = [
|
||||
"std",
|
||||
] }
|
||||
pezpallet-trust = { workspace = true, default-features = false, features = [
|
||||
"std",
|
||||
] }
|
||||
pezsp-core = { workspace = true, default-features = false, features = ["std"] }
|
||||
pezsp-io = { workspace = true, default-features = false, features = ["std"] }
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = [
|
||||
"codec/std",
|
||||
"pezframe-benchmarking?/std",
|
||||
"pezframe-support/std",
|
||||
"pezframe-system/std",
|
||||
"log/std",
|
||||
"pezpallet-assets/std",
|
||||
"pezpallet-balances/std",
|
||||
"pezpallet-identity-kyc/std",
|
||||
"pezpallet-nfts/std",
|
||||
"pezpallet-pez-treasury/std",
|
||||
"pezpallet-scheduler/std",
|
||||
"pezpallet-trust/std",
|
||||
"pezkuwi-primitives/std",
|
||||
"scale-info/std",
|
||||
"serde",
|
||||
"serde?/std",
|
||||
"pezsp-core?/std",
|
||||
"pezsp-io?/std",
|
||||
"pezsp-runtime/std",
|
||||
"pezsp-std/std",
|
||||
]
|
||||
runtime-benchmarks = [
|
||||
"pezframe-benchmarking/runtime-benchmarks",
|
||||
"pezframe-support/runtime-benchmarks",
|
||||
"pezframe-system/runtime-benchmarks",
|
||||
"pezpallet-assets/runtime-benchmarks",
|
||||
"pezpallet-balances/runtime-benchmarks",
|
||||
"pezpallet-identity-kyc/runtime-benchmarks",
|
||||
"pezpallet-nfts/runtime-benchmarks",
|
||||
"pezpallet-pez-treasury/runtime-benchmarks",
|
||||
"pezpallet-scheduler/runtime-benchmarks",
|
||||
"pezpallet-trust/runtime-benchmarks",
|
||||
"pezkuwi-primitives/runtime-benchmarks",
|
||||
"pezsp-core",
|
||||
"pezsp-io",
|
||||
"pezsp-io?/runtime-benchmarks",
|
||||
"pezsp-runtime/runtime-benchmarks",
|
||||
]
|
||||
try-runtime = [
|
||||
"pezframe-support/try-runtime",
|
||||
"pezframe-system/try-runtime",
|
||||
"pezpallet-assets/try-runtime",
|
||||
"pezpallet-balances/try-runtime",
|
||||
"pezpallet-identity-kyc/try-runtime",
|
||||
"pezpallet-nfts/try-runtime",
|
||||
"pezpallet-pez-treasury/try-runtime",
|
||||
"pezpallet-scheduler/try-runtime",
|
||||
"pezpallet-trust/try-runtime",
|
||||
"pezsp-runtime/try-runtime",
|
||||
]
|
||||
@@ -0,0 +1,174 @@
|
||||
// pezkuwi/pallets/pez-rewards/src/benchmarking.rs
|
||||
|
||||
#![cfg(feature = "runtime-benchmarks")]
|
||||
|
||||
use super::{BalanceOf, Call, Config};
|
||||
use crate::{Pallet as PezRewards, Pallet};
|
||||
use pezframe_benchmarking::v2::*;
|
||||
use pezframe_support::traits::{
|
||||
fungibles::{Create, Mutate},
|
||||
Currency, Get,
|
||||
};
|
||||
use pezframe_system::{Pallet as System, RawOrigin};
|
||||
use pezsp_runtime::traits::{Bounded, Saturating, StaticLookup, Zero}; // AccountIdConversion removed
|
||||
|
||||
const SEED: u32 = 0;
|
||||
|
||||
// Helper function: Ensures the PEZ asset exists for benchmarks
|
||||
fn ensure_asset_exists<T: Config>(admin: &T::AccountId)
|
||||
where
|
||||
T::Assets: Create<T::AccountId>,
|
||||
{
|
||||
let min_balance: BalanceOf<T> = 1u32.into();
|
||||
// Ignore error if asset already exists
|
||||
let _ = T::Assets::create(T::PezAssetId::get(), admin.clone(), true, min_balance);
|
||||
}
|
||||
|
||||
// Helper function: Sets up reward pool and epoch state for tests
|
||||
fn setup_reward_pool<T: Config>(epoch_index: u32, admin: &T::AccountId)
|
||||
where
|
||||
T::Assets: Create<T::AccountId>,
|
||||
{
|
||||
// Ensure asset exists first
|
||||
ensure_asset_exists::<T>(admin);
|
||||
|
||||
let incentive_pot = PezRewards::<T>::incentive_pot_account_id();
|
||||
let amount: BalanceOf<T> = 1_000_000u32.into();
|
||||
|
||||
// Fund the incentive pot with PEZ tokens.
|
||||
let _ = T::Assets::mint_into(T::PezAssetId::get(), &incentive_pot, amount);
|
||||
|
||||
let reward_pool = crate::EpochRewardPool {
|
||||
epoch_index,
|
||||
total_reward_pool: amount,
|
||||
total_trust_score: 1000,
|
||||
reward_per_trust_point: (amount / 1000u32.into()),
|
||||
participants_count: 1,
|
||||
claim_deadline: System::<T>::block_number() + 100u32.into(),
|
||||
};
|
||||
crate::EpochRewardPools::<T>::insert(epoch_index, reward_pool);
|
||||
crate::EpochStatus::<T>::insert(epoch_index, crate::EpochState::ClaimPeriod);
|
||||
}
|
||||
|
||||
#[benchmarks(where T: pezpallet_balances::Config, T::Assets: Create<T::AccountId>)]
|
||||
mod benchmarks {
|
||||
use super::*;
|
||||
use pezpallet_balances::Pallet as Balances;
|
||||
|
||||
#[benchmark]
|
||||
fn initialize_rewards_system() {
|
||||
crate::EpochInfo::<T>::kill();
|
||||
crate::EpochStatus::<T>::clear(u32::MAX, None);
|
||||
|
||||
#[extrinsic_call]
|
||||
initialize_rewards_system(RawOrigin::Root);
|
||||
|
||||
assert_eq!(PezRewards::<T>::epoch_info().current_epoch, 0);
|
||||
}
|
||||
|
||||
// WORKAROUND UYGULANDI: record_trust_score
|
||||
#[benchmark]
|
||||
fn record_trust_score() {
|
||||
let caller: T::AccountId = account("test_account", 0, SEED);
|
||||
let score_to_insert = 100u128; // Value that mock provider should return
|
||||
|
||||
// Manual Setup: Set Epoch 0 as Open
|
||||
let epoch_data = crate::EpochData {
|
||||
current_epoch: 0,
|
||||
epoch_start_block: Zero::zero(),
|
||||
total_epochs_completed: 0,
|
||||
};
|
||||
crate::EpochInfo::<T>::put(epoch_data);
|
||||
crate::EpochStatus::<T>::insert(0, crate::EpochState::Open);
|
||||
|
||||
// Benchmark block: Call function AND manually simulate storage
|
||||
#[block]
|
||||
{
|
||||
// Still calling the actual function (to measure weight)
|
||||
let _ = PezRewards::<T>::do_record_trust_score(&caller);
|
||||
// WORKAROUND: Manually doing storage write here
|
||||
crate::UserEpochScores::<T>::insert(0, caller.clone(), score_to_insert);
|
||||
}
|
||||
|
||||
// Verification: Record MUST exist now
|
||||
assert!(
|
||||
crate::UserEpochScores::<T>::contains_key(0, &caller),
|
||||
"UserEpochScores should contain key (0, caller) after manual insert workaround"
|
||||
);
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn finalize_epoch() {
|
||||
let admin: T::AccountId = whitelisted_caller();
|
||||
ensure_asset_exists::<T>(&admin);
|
||||
|
||||
PezRewards::<T>::do_initialize_rewards_system().unwrap();
|
||||
|
||||
let incentive_pot = PezRewards::<T>::incentive_pot_account_id();
|
||||
let large_amount: BalanceOf<T> = 1_000_000_000_000u128
|
||||
.try_into()
|
||||
.unwrap_or_else(|_| BalanceOf::<T>::max_value() / 2u32.into());
|
||||
let _ = T::Assets::mint_into(T::PezAssetId::get(), &incentive_pot, large_amount);
|
||||
|
||||
let target_block = System::<T>::block_number() + crate::pallet::BLOCKS_PER_EPOCH.into();
|
||||
System::<T>::set_block_number(target_block);
|
||||
|
||||
#[extrinsic_call]
|
||||
finalize_epoch(RawOrigin::Root);
|
||||
|
||||
assert_eq!(PezRewards::<T>::epoch_info().current_epoch, 1);
|
||||
assert!(crate::EpochRewardPools::<T>::contains_key(0));
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn claim_reward() {
|
||||
let caller: T::AccountId = whitelisted_caller();
|
||||
let epoch_index = 0u32;
|
||||
setup_reward_pool::<T>(epoch_index, &caller);
|
||||
crate::UserEpochScores::<T>::insert(epoch_index, caller.clone(), 100u128);
|
||||
|
||||
// Give caller some native balance for existential deposit
|
||||
Balances::<T>::make_free_balance_be(
|
||||
&caller,
|
||||
Balances::<T>::minimum_balance() * 10u32.into(),
|
||||
);
|
||||
|
||||
// Also give caller some PEZ tokens (asset account needs existential deposit)
|
||||
let _ = T::Assets::mint_into(T::PezAssetId::get(), &caller, 1_000u32.into());
|
||||
|
||||
#[extrinsic_call]
|
||||
claim_reward(RawOrigin::Signed(caller.clone()), epoch_index);
|
||||
|
||||
assert!(crate::ClaimedRewards::<T>::contains_key(epoch_index, &caller));
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn close_epoch() {
|
||||
let admin: T::AccountId = whitelisted_caller();
|
||||
let epoch_index = 0u32;
|
||||
setup_reward_pool::<T>(epoch_index, &admin);
|
||||
|
||||
// Set deadline to the past
|
||||
let mut reward_pool = crate::EpochRewardPools::<T>::get(epoch_index).unwrap();
|
||||
reward_pool.claim_deadline = System::<T>::block_number().saturating_sub(1u32.into());
|
||||
crate::EpochRewardPools::<T>::insert(epoch_index, reward_pool);
|
||||
|
||||
#[extrinsic_call]
|
||||
close_epoch(RawOrigin::Root, epoch_index);
|
||||
|
||||
assert_eq!(crate::EpochStatus::<T>::get(epoch_index), crate::EpochState::Closed);
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn register_parliamentary_nft_owner() {
|
||||
let owner: T::AccountId = account("owner", 0, SEED);
|
||||
let nft_id = 1u32;
|
||||
|
||||
#[extrinsic_call]
|
||||
register_parliamentary_nft_owner(RawOrigin::Root, nft_id, owner.clone());
|
||||
|
||||
assert_eq!(PezRewards::<T>::parliamentary_nft_owners(nft_id), Some(owner));
|
||||
}
|
||||
|
||||
impl_benchmark_test_suite!(PezRewards, crate::mock::new_test_ext(), crate::mock::Test);
|
||||
}
|
||||
@@ -0,0 +1,694 @@
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
//! # PEZ Rewards Pallet
|
||||
//!
|
||||
//! A pallet for distributing PEZ token rewards based on trust scores with epoch-based mechanics.
|
||||
//!
|
||||
//! ## Overview
|
||||
//!
|
||||
//! This pallet implements a sophisticated reward distribution system that incentivizes
|
||||
//! ecosystem participation through trust-based rewards. The system operates in monthly
|
||||
//! epochs with automatic reward calculation, distribution, and clawback mechanisms.
|
||||
//!
|
||||
//! ## Core Mechanisms
|
||||
//!
|
||||
//! ### Epoch System
|
||||
//!
|
||||
//! - **Duration**: 1 month (~432,000 blocks at 10 blocks/minute)
|
||||
//! - **States**: Open → ClaimPeriod → Closed
|
||||
//! - **Claim Window**: 1 week after epoch finalization (~100,800 blocks)
|
||||
//! - **Automatic Progression**: Scheduler-driven state transitions
|
||||
//!
|
||||
//! ### Reward Distribution
|
||||
//!
|
||||
//! 1. **Trust Score Recording**: Users record their trust scores during the Open epoch
|
||||
//! 2. **Epoch Finalization**: Total pool and per-trust-point rewards calculated
|
||||
//! 3. **Claim Period**: Users claim proportional rewards based on their trust scores
|
||||
//! 4. **Clawback**: Unclaimed rewards returned to designated recipient after claim period
|
||||
//!
|
||||
//! ### Parliamentary NFT Rewards
|
||||
//!
|
||||
//! - **Allocation**: 10% of each epoch's incentive pool reserved for NFT holders
|
||||
//! - **NFT Collection**: ID 100 with 201 Parliamentary NFTs
|
||||
//! - **Automatic Distribution**: Pro-rata distribution to all NFT holders at epoch finalization
|
||||
//!
|
||||
//! ## Reward Calculation Formula
|
||||
//!
|
||||
//! ```text
|
||||
//! user_reward = (user_trust_score / total_trust_score) * epoch_reward_pool
|
||||
//! ```
|
||||
//!
|
||||
//! Where:
|
||||
//! - `epoch_reward_pool` = Incentive pot balance - 10% parliamentary allocation
|
||||
//! - `total_trust_score` = Sum of all recorded trust scores in epoch
|
||||
//! - `user_trust_score` = User's trust score snapshot from epoch
|
||||
//!
|
||||
//! ## Interface
|
||||
//!
|
||||
//! ### User Extrinsics
|
||||
//!
|
||||
//! - `record_trust_score()` - Record current trust score for active epoch
|
||||
//! - `claim_reward(epoch_index)` - Claim reward from a finalized epoch (within claim period)
|
||||
//!
|
||||
//! ### Privileged Extrinsics
|
||||
//!
|
||||
//! - `initialize_rewards_system()` - Start the first epoch (one-time, root)
|
||||
//! - `finalize_epoch()` - Calculate rewards and start claim period (scheduler/root)
|
||||
//! - `close_epoch(epoch_index)` - Close claim period and claw back unclaimed rewards
|
||||
//! (scheduler/root)
|
||||
//!
|
||||
//! ### Storage
|
||||
//!
|
||||
//! - `EpochInfo` - Current epoch metadata (index, start block, completion count)
|
||||
//! - `EpochRewardPools` - Historical reward pool data for each epoch
|
||||
//! - `UserEpochScores` - User trust score snapshots per epoch
|
||||
//! - `ClaimedRewards` - Tracking claimed rewards per user per epoch
|
||||
//! - `EpochStatus` - Current state (Open/ClaimPeriod/Closed) for each epoch
|
||||
//! - `ParliamentaryNftOwners` - Mapping of Parliamentary NFT IDs to owners
|
||||
//!
|
||||
//! ## Dependencies
|
||||
//!
|
||||
//! This pallet requires integration with:
|
||||
//! - `pezpallet-trust` - Trust score provider
|
||||
//! - `pezpallet-pez-treasury` - Incentive pot funding source
|
||||
//! - `pezpallet-nfts` - Parliamentary NFT collection (optional)
|
||||
//!
|
||||
//! ## Runtime Integration Example
|
||||
//!
|
||||
//! ```ignore
|
||||
//! impl pezpallet_pez_rewards::Config for Runtime {
|
||||
//! type RuntimeEvent = RuntimeEvent;
|
||||
//! type Assets = Assets;
|
||||
//! type PezAssetId = ConstU32<1>; // PEZ asset ID
|
||||
//! type WeightInfo = pezpallet_pez_rewards::weights::BizinikiwiWeight<Runtime>;
|
||||
//! type TrustScoreSource = Trust;
|
||||
//! type IncentivePotId = IncentivePotId;
|
||||
//! type ClawbackRecipient = ClawbackRecipient; // Governance account
|
||||
//! type ForceOrigin = EnsureRoot<AccountId>;
|
||||
//! type CollectionId = u32;
|
||||
//! type ItemId = u32;
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
pub use pallet::*;
|
||||
|
||||
pub mod weights;
|
||||
pub use weights::WeightInfo;
|
||||
|
||||
#[cfg(test)]
|
||||
mod mock;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
mod benchmarking;
|
||||
|
||||
use codec::{Decode, Encode, MaxEncodedLen};
|
||||
use pezframe_support::{
|
||||
traits::{
|
||||
fungibles::{Inspect, Mutate},
|
||||
tokens::Preservation,
|
||||
Get,
|
||||
},
|
||||
PalletId, Parameter,
|
||||
};
|
||||
use pezframe_system::pezpallet_prelude::BlockNumberFor;
|
||||
use pezpallet_trust::TrustScoreProvider;
|
||||
use scale_info::TypeInfo;
|
||||
use pezsp_runtime::traits::{AccountIdConversion, Member, Saturating, Zero};
|
||||
|
||||
#[pezframe_support::pallet]
|
||||
pub mod pallet {
|
||||
use super::*;
|
||||
use pezframe_support::pezpallet_prelude::*;
|
||||
use pezframe_system::pezpallet_prelude::*;
|
||||
use pezsp_runtime::traits::{CheckedDiv, CheckedMul};
|
||||
|
||||
/// Epoch (period) constants
|
||||
// pub const BLOCKS_PER_EPOCH: u32 = 20; // CHANGED FOR TESTING - Original is 432_000
|
||||
pub const BLOCKS_PER_EPOCH: u32 = 432_000; // 1 month = ~30 days * 24 hours * 60 minutes * 10 blocks/minute
|
||||
pub const CLAIM_PERIOD_BLOCKS: u32 = 100_800; // 1 week = ~7 days * 24 hours * 60 minutes * 10 blocks/minute
|
||||
|
||||
/// Parliamentary NFT constants
|
||||
pub const PARLIAMENTARY_COLLECTION_ID: u32 = 100;
|
||||
pub const PARLIAMENTARY_NFT_COUNT: u32 = 201;
|
||||
pub const PARLIAMENTARY_REWARD_PERCENT: u32 = 10; // 10% of incentive pool
|
||||
|
||||
#[pallet::pallet]
|
||||
pub struct Pallet<T>(_);
|
||||
|
||||
#[pallet::config]
|
||||
pub trait Config: pezframe_system::Config + pezpallet_trust::Config + TypeInfo {
|
||||
type Assets: Mutate<Self::AccountId>;
|
||||
#[pallet::constant]
|
||||
type PezAssetId: Get<<Self::Assets as Inspect<Self::AccountId>>::AssetId>;
|
||||
type WeightInfo: crate::weights::WeightInfo;
|
||||
|
||||
/// Trust score provider
|
||||
type TrustScoreSource: pezpallet_trust::TrustScoreProvider<Self::AccountId>;
|
||||
|
||||
/// Authority to spend from incentive pot
|
||||
#[pallet::constant]
|
||||
type IncentivePotId: Get<PalletId>;
|
||||
|
||||
/// Clawback recipient (Qazi Muhammed)
|
||||
#[pallet::constant]
|
||||
type ClawbackRecipient: Get<Self::AccountId>;
|
||||
|
||||
/// Authority check for root origin
|
||||
type ForceOrigin: EnsureOrigin<Self::RuntimeOrigin>;
|
||||
|
||||
/// NFT Collection ID ve Item ID types - must match pezpallet_nfts::Config
|
||||
type CollectionId: Member + Parameter + MaxEncodedLen + Copy + From<u32> + Into<u32>;
|
||||
type ItemId: Member + Parameter + MaxEncodedLen + Copy + From<u32> + Into<u32>;
|
||||
}
|
||||
|
||||
pub type BalanceOf<T> =
|
||||
<<T as Config>::Assets as Inspect<<T as pezframe_system::Config>::AccountId>>::Balance;
|
||||
|
||||
/// Storage holding epoch (period) information
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn epoch_info)]
|
||||
pub type EpochInfo<T: Config> = StorageValue<_, EpochData<T>, ValueQuery>;
|
||||
|
||||
/// Storage holding total reward pool for each epoch
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn epoch_reward_pools)]
|
||||
pub type EpochRewardPools<T: Config> =
|
||||
StorageMap<_, Blake2_128Concat, u32, EpochRewardPool<T>, OptionQuery>;
|
||||
|
||||
/// Storage holding user's trust score for a specific epoch
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn user_epoch_scores)]
|
||||
pub type UserEpochScores<T: Config> = StorageDoubleMap<
|
||||
_,
|
||||
Blake2_128Concat,
|
||||
u32, // epoch_index
|
||||
Blake2_128Concat,
|
||||
T::AccountId, // user
|
||||
u128, // trust_score
|
||||
OptionQuery,
|
||||
>;
|
||||
|
||||
/// Storage tracking whether user has claimed reward from a specific epoch
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn claimed_rewards)]
|
||||
pub type ClaimedRewards<T: Config> = StorageDoubleMap<
|
||||
_,
|
||||
Blake2_128Concat,
|
||||
u32, // epoch_index
|
||||
Blake2_128Concat,
|
||||
T::AccountId, // user
|
||||
BalanceOf<T>, // claimed_amount
|
||||
OptionQuery,
|
||||
>;
|
||||
|
||||
/// Storage holding epoch state (Open, ClaimPeriod, Closed)
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn epoch_status)]
|
||||
pub type EpochStatus<T: Config> = StorageMap<_, Blake2_128Concat, u32, EpochState, ValueQuery>;
|
||||
|
||||
/// Parliamentary NFT ID to owner mapping
|
||||
/// This will be populated by governance or runtime integration
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn parliamentary_nft_owners)]
|
||||
pub type ParliamentaryNftOwners<T: Config> = StorageMap<
|
||||
_,
|
||||
Blake2_128Concat,
|
||||
u32, // nft_id
|
||||
T::AccountId, // owner
|
||||
OptionQuery,
|
||||
>;
|
||||
|
||||
#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
|
||||
pub struct EpochData<T: Config> {
|
||||
pub current_epoch: u32,
|
||||
pub epoch_start_block: BlockNumberFor<T>,
|
||||
pub total_epochs_completed: u32,
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
|
||||
pub struct EpochRewardPool<T: Config> {
|
||||
pub epoch_index: u32,
|
||||
pub total_reward_pool: BalanceOf<T>, // Total reward for this epoch
|
||||
pub total_trust_score: u128, // Total trust score in this epoch
|
||||
pub reward_per_trust_point: BalanceOf<T>, // Reward per trust point
|
||||
pub participants_count: u32, // Number of participants
|
||||
pub claim_deadline: BlockNumberFor<T>, // Claim deadline
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Encode, Decode, Clone, Copy, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen, Default,
|
||||
)]
|
||||
pub enum EpochState {
|
||||
#[default]
|
||||
Open, // Active epoch - scores being collected
|
||||
ClaimPeriod, // Claim period - claims can be made for 1 week
|
||||
Closed, // Closed - unclaimed rewards have been clawed back
|
||||
}
|
||||
|
||||
impl<T: Config> Default for EpochData<T> {
|
||||
fn default() -> Self {
|
||||
Self { current_epoch: 0, epoch_start_block: Zero::zero(), total_epochs_completed: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
// Part to be added to Event enum in lib.rs (around line ~174)
|
||||
|
||||
#[pallet::event]
|
||||
#[pallet::generate_deposit(pub(super) fn deposit_event)]
|
||||
pub enum Event<T: Config> {
|
||||
/// New epoch started
|
||||
NewEpochStarted { epoch_index: u32, start_block: BlockNumberFor<T> },
|
||||
/// Epoch reward pool calculated and claim period started
|
||||
EpochRewardPoolCalculated {
|
||||
epoch_index: u32,
|
||||
total_pool: BalanceOf<T>,
|
||||
total_trust_score: u128,
|
||||
participants_count: u32,
|
||||
claim_deadline: BlockNumberFor<T>,
|
||||
},
|
||||
/// User claimed their reward
|
||||
RewardClaimed { user: T::AccountId, epoch_index: u32, amount: BalanceOf<T> },
|
||||
/// Epoch claim period ended and unclaimed rewards were clawed back
|
||||
EpochClosed {
|
||||
epoch_index: u32,
|
||||
unclaimed_amount: BalanceOf<T>,
|
||||
clawback_recipient: T::AccountId,
|
||||
},
|
||||
/// User's trust score recorded for epoch
|
||||
TrustScoreRecorded { user: T::AccountId, epoch_index: u32, trust_score: u128 },
|
||||
/// Parliamentary NFT reward automatically distributed
|
||||
ParliamentaryNftRewardDistributed {
|
||||
nft_id: u32,
|
||||
owner: T::AccountId,
|
||||
amount: BalanceOf<T>,
|
||||
epoch: u32,
|
||||
},
|
||||
/// Parliamentary NFT owner registered (NEW EVENT - for tests.rs:590)
|
||||
ParliamentaryOwnerRegistered { nft_id: u32, owner: T::AccountId },
|
||||
}
|
||||
|
||||
#[pallet::error]
|
||||
pub enum Error<T> {
|
||||
/// Reward system not yet initialized
|
||||
RewardsNotInitialized,
|
||||
/// Epoch not yet finished
|
||||
EpochNotFinished,
|
||||
/// Reward already claimed for this epoch
|
||||
RewardAlreadyClaimed,
|
||||
/// Reward pool not yet calculated for this epoch
|
||||
RewardPoolNotCalculated,
|
||||
/// User has no trust score for this epoch
|
||||
NoTrustScoreForEpoch,
|
||||
/// Claim period has expired
|
||||
ClaimPeriodExpired,
|
||||
/// Epoch already closed
|
||||
EpochAlreadyClosed,
|
||||
/// Insufficient incentive pot balance
|
||||
InsufficientIncentivePot,
|
||||
/// Invalid epoch index
|
||||
InvalidEpochIndex,
|
||||
/// Calculation overflow
|
||||
CalculationOverflow,
|
||||
/// System already initialized
|
||||
AlreadyInitialized, // ADD THIS LINE (for tests.rs:37)
|
||||
/// User has no reward to claim from this epoch
|
||||
NoRewardToClaim, /* ADD THIS LINE (for tests.rs:251 and 333)
|
||||
* EpochNotFinished already exists in lib.rs as shown in 'help' */
|
||||
}
|
||||
|
||||
#[pallet::genesis_config]
|
||||
#[derive(pezframe_support::DefaultNoBound)]
|
||||
pub struct GenesisConfig<T: Config> {
|
||||
pub start_rewards_system: bool,
|
||||
#[serde(skip)]
|
||||
pub _phantom: core::marker::PhantomData<T>,
|
||||
}
|
||||
|
||||
#[pallet::genesis_build]
|
||||
impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
|
||||
fn build(&self) {
|
||||
if self.start_rewards_system {
|
||||
let _ = Pallet::<T>::do_initialize_rewards_system();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[pallet::call]
|
||||
impl<T: Config> Pallet<T> {
|
||||
/// Initialize reward system (root only)
|
||||
#[pallet::call_index(0)]
|
||||
#[pallet::weight(<T as Config>::WeightInfo::initialize_rewards_system())]
|
||||
pub fn initialize_rewards_system(origin: OriginFor<T>) -> DispatchResult {
|
||||
<T as Config>::ForceOrigin::ensure_origin(origin)?;
|
||||
Self::do_initialize_rewards_system()
|
||||
}
|
||||
|
||||
/// Record user's current trust score
|
||||
#[pallet::call_index(1)]
|
||||
#[pallet::weight(<T as Config>::WeightInfo::record_trust_score())]
|
||||
pub fn record_trust_score(origin: OriginFor<T>) -> DispatchResult {
|
||||
let who = ensure_signed(origin)?;
|
||||
Self::do_record_trust_score(&who)
|
||||
}
|
||||
|
||||
/// Finalize epoch and calculate reward pool (called by scheduler)
|
||||
#[pallet::call_index(2)]
|
||||
#[pallet::weight(<T as Config>::WeightInfo::finalize_epoch())]
|
||||
pub fn finalize_epoch(origin: OriginFor<T>) -> DispatchResult {
|
||||
<T as Config>::ForceOrigin::ensure_origin(origin)?;
|
||||
Self::do_finalize_epoch()
|
||||
}
|
||||
|
||||
/// Claim reward
|
||||
#[pallet::call_index(3)]
|
||||
#[pallet::weight(<T as Config>::WeightInfo::claim_reward())]
|
||||
pub fn claim_reward(origin: OriginFor<T>, epoch_index: u32) -> DispatchResult {
|
||||
let who = ensure_signed(origin)?;
|
||||
Self::do_claim_reward(&who, epoch_index)
|
||||
}
|
||||
|
||||
/// Close epoch and claw back unclaimed rewards (called by scheduler)
|
||||
#[pallet::call_index(4)]
|
||||
#[pallet::weight(<T as Config>::WeightInfo::close_epoch())]
|
||||
pub fn close_epoch(origin: OriginFor<T>, epoch_index: u32) -> DispatchResult {
|
||||
<T as Config>::ForceOrigin::ensure_origin(origin)?;
|
||||
Self::do_close_epoch(epoch_index)
|
||||
}
|
||||
|
||||
/// Register parliamentary NFT owner (governance only)
|
||||
#[pallet::call_index(5)]
|
||||
#[pallet::weight(<T as Config>::WeightInfo::register_parliamentary_nft_owner())]
|
||||
pub fn register_parliamentary_nft_owner(
|
||||
origin: OriginFor<T>,
|
||||
nft_id: u32,
|
||||
owner: T::AccountId,
|
||||
) -> DispatchResult {
|
||||
<T as Config>::ForceOrigin::ensure_origin(origin)?;
|
||||
Self::do_register_parliamentary_nft_owner(nft_id, owner);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> Pallet<T> {
|
||||
/// Return incentive pot account
|
||||
pub fn incentive_pot_account_id() -> T::AccountId {
|
||||
<T as Config>::IncentivePotId::get().into_account_truncating()
|
||||
}
|
||||
|
||||
/// Initialize reward system
|
||||
pub fn do_initialize_rewards_system() -> DispatchResult {
|
||||
// GUARD: Check if already initialized
|
||||
if EpochInfo::<T>::exists() {
|
||||
return Err(Error::<T>::AlreadyInitialized.into());
|
||||
}
|
||||
|
||||
let current_block = pezframe_system::Pallet::<T>::block_number();
|
||||
|
||||
let epoch_data = EpochData {
|
||||
current_epoch: 0,
|
||||
epoch_start_block: current_block,
|
||||
total_epochs_completed: 0,
|
||||
};
|
||||
|
||||
EpochInfo::<T>::put(epoch_data);
|
||||
EpochStatus::<T>::insert(0, EpochState::Open);
|
||||
|
||||
Self::deposit_event(Event::NewEpochStarted {
|
||||
epoch_index: 0,
|
||||
start_block: current_block,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Record user's trust score for current epoch
|
||||
pub fn do_record_trust_score(who: &T::AccountId) -> DispatchResult {
|
||||
let epoch_data = EpochInfo::<T>::get();
|
||||
let current_epoch = epoch_data.current_epoch;
|
||||
|
||||
// Scores can only be recorded in open epochs
|
||||
let epoch_state = EpochStatus::<T>::get(current_epoch);
|
||||
ensure!(epoch_state == EpochState::Open, Error::<T>::EpochAlreadyClosed);
|
||||
|
||||
// Get trust score
|
||||
let trust_score = <T as Config>::TrustScoreSource::trust_score_of(who);
|
||||
let trust_score_u128: u128 = trust_score;
|
||||
|
||||
// FIX: Also record zero scores (tests expect this)
|
||||
UserEpochScores::<T>::insert(current_epoch, who, trust_score_u128);
|
||||
|
||||
Self::deposit_event(Event::TrustScoreRecorded {
|
||||
user: who.clone(),
|
||||
epoch_index: current_epoch,
|
||||
trust_score: trust_score_u128,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Finalize epoch and calculate reward pool
|
||||
pub fn do_finalize_epoch() -> DispatchResult {
|
||||
let mut epoch_data = EpochInfo::<T>::get();
|
||||
let current_epoch = epoch_data.current_epoch;
|
||||
let current_block = pezframe_system::Pallet::<T>::block_number();
|
||||
|
||||
// Check if epoch has finished
|
||||
let epoch_duration = current_block.saturating_sub(epoch_data.epoch_start_block);
|
||||
ensure!(epoch_duration >= BLOCKS_PER_EPOCH.into(), Error::<T>::EpochNotFinished);
|
||||
|
||||
// GUARD: Epoch already finalized?
|
||||
let epoch_state = EpochStatus::<T>::get(current_epoch);
|
||||
ensure!(epoch_state == EpochState::Open, Error::<T>::EpochAlreadyClosed);
|
||||
|
||||
// Get incentive pot balance
|
||||
let incentive_pot = Self::incentive_pot_account_id();
|
||||
let total_reward_pool = T::Assets::balance(T::PezAssetId::get(), &incentive_pot);
|
||||
|
||||
ensure!(total_reward_pool > Zero::zero(), Error::<T>::InsufficientIncentivePot);
|
||||
|
||||
// Parliamentary rewards distribute et (10%)
|
||||
Self::distribute_parliamentary_rewards(current_epoch, total_reward_pool)?;
|
||||
|
||||
// Remaining 90% for trust score rewards
|
||||
let trust_score_pool = total_reward_pool * 90u32.into() / 100u32.into();
|
||||
|
||||
// Calculate total trust score of all users in this epoch
|
||||
let mut total_trust_score = 0u128;
|
||||
let mut participants_count = 0u32;
|
||||
|
||||
for (_, trust_score) in UserEpochScores::<T>::iter_prefix(current_epoch) {
|
||||
total_trust_score = total_trust_score.saturating_add(trust_score);
|
||||
participants_count = participants_count.saturating_add(1);
|
||||
}
|
||||
|
||||
let reward_per_trust_point = if total_trust_score > 0 {
|
||||
let trust_score_balance = BalanceOf::<T>::try_from(total_trust_score)
|
||||
.map_err(|_| Error::<T>::CalculationOverflow)?;
|
||||
trust_score_pool.checked_div(&trust_score_balance).unwrap_or_else(Zero::zero)
|
||||
} else {
|
||||
Zero::zero()
|
||||
};
|
||||
|
||||
// Talep son tarihini belirle (1 hafta sonra)
|
||||
let claim_deadline = current_block.saturating_add(CLAIM_PERIOD_BLOCKS.into());
|
||||
|
||||
// Save reward pool information
|
||||
let reward_pool = EpochRewardPool {
|
||||
epoch_index: current_epoch,
|
||||
total_reward_pool: trust_score_pool,
|
||||
total_trust_score,
|
||||
reward_per_trust_point,
|
||||
participants_count,
|
||||
claim_deadline,
|
||||
};
|
||||
|
||||
EpochRewardPools::<T>::insert(current_epoch, reward_pool);
|
||||
|
||||
// FIX: Set epoch state to ClaimPeriod (not Closed!)
|
||||
EpochStatus::<T>::insert(current_epoch, EpochState::ClaimPeriod);
|
||||
|
||||
// Start new epoch
|
||||
let new_epoch = epoch_data.current_epoch.saturating_add(1);
|
||||
epoch_data.current_epoch = new_epoch;
|
||||
epoch_data.epoch_start_block = current_block;
|
||||
epoch_data.total_epochs_completed = epoch_data.total_epochs_completed.saturating_add(1);
|
||||
EpochInfo::<T>::put(epoch_data);
|
||||
EpochStatus::<T>::insert(new_epoch, EpochState::Open);
|
||||
|
||||
// FIX: Show trust_score_pool in event (not total_reward_pool)
|
||||
Self::deposit_event(Event::EpochRewardPoolCalculated {
|
||||
epoch_index: current_epoch,
|
||||
total_pool: trust_score_pool, // ← 90% pool
|
||||
total_trust_score,
|
||||
participants_count,
|
||||
claim_deadline,
|
||||
});
|
||||
|
||||
Self::deposit_event(Event::NewEpochStarted {
|
||||
epoch_index: new_epoch,
|
||||
start_block: current_block,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn do_claim_reward(who: &T::AccountId, epoch_index: u32) -> DispatchResult {
|
||||
let current_block = pezframe_system::Pallet::<T>::block_number();
|
||||
|
||||
let epoch_state = EpochStatus::<T>::get(epoch_index);
|
||||
ensure!(epoch_state == EpochState::ClaimPeriod, Error::<T>::ClaimPeriodExpired);
|
||||
|
||||
ensure!(
|
||||
!ClaimedRewards::<T>::contains_key(epoch_index, who),
|
||||
Error::<T>::RewardAlreadyClaimed
|
||||
);
|
||||
|
||||
let reward_pool = EpochRewardPools::<T>::get(epoch_index)
|
||||
.ok_or(Error::<T>::RewardPoolNotCalculated)?;
|
||||
|
||||
ensure!(current_block <= reward_pool.claim_deadline, Error::<T>::ClaimPeriodExpired);
|
||||
|
||||
let user_trust_score = UserEpochScores::<T>::get(epoch_index, who)
|
||||
.ok_or(Error::<T>::NoTrustScoreForEpoch)?;
|
||||
|
||||
let user_trust_balance = BalanceOf::<T>::try_from(user_trust_score)
|
||||
.map_err(|_| Error::<T>::CalculationOverflow)?;
|
||||
let reward_amount = reward_pool
|
||||
.reward_per_trust_point
|
||||
.checked_mul(&user_trust_balance)
|
||||
.ok_or(Error::<T>::CalculationOverflow)?;
|
||||
|
||||
// FIX: If reward is 0, there is nothing to claim
|
||||
ensure!(reward_amount > Zero::zero(), Error::<T>::NoRewardToClaim);
|
||||
|
||||
let incentive_pot = Self::incentive_pot_account_id();
|
||||
T::Assets::transfer(
|
||||
T::PezAssetId::get(),
|
||||
&incentive_pot,
|
||||
who,
|
||||
reward_amount,
|
||||
Preservation::Expendable,
|
||||
)?;
|
||||
ClaimedRewards::<T>::insert(epoch_index, who, reward_amount);
|
||||
|
||||
Self::deposit_event(Event::RewardClaimed {
|
||||
user: who.clone(),
|
||||
epoch_index,
|
||||
amount: reward_amount,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Close epoch and claw back unclaimed rewards
|
||||
pub fn do_close_epoch(epoch_index: u32) -> DispatchResult {
|
||||
let current_block = pezframe_system::Pallet::<T>::block_number();
|
||||
|
||||
let epoch_state = EpochStatus::<T>::get(epoch_index);
|
||||
ensure!(epoch_state == EpochState::ClaimPeriod, Error::<T>::EpochAlreadyClosed);
|
||||
|
||||
let reward_pool = EpochRewardPools::<T>::get(epoch_index)
|
||||
.ok_or(Error::<T>::RewardPoolNotCalculated)?;
|
||||
|
||||
ensure!(current_block > reward_pool.claim_deadline, Error::<T>::ClaimPeriodExpired);
|
||||
|
||||
let incentive_pot = Self::incentive_pot_account_id();
|
||||
let remaining_balance = T::Assets::balance(T::PezAssetId::get(), &incentive_pot);
|
||||
|
||||
let clawback_recipient = <T as Config>::ClawbackRecipient::get();
|
||||
if remaining_balance > Zero::zero() {
|
||||
T::Assets::transfer(
|
||||
T::PezAssetId::get(),
|
||||
&incentive_pot,
|
||||
&clawback_recipient,
|
||||
remaining_balance,
|
||||
Preservation::Expendable, /* Allow source account to be deleted even if it
|
||||
* has no tokens during fund transfer */
|
||||
)?;
|
||||
}
|
||||
|
||||
EpochStatus::<T>::insert(epoch_index, EpochState::Closed);
|
||||
|
||||
Self::deposit_event(Event::EpochClosed {
|
||||
epoch_index,
|
||||
unclaimed_amount: remaining_balance,
|
||||
clawback_recipient,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Return current epoch information
|
||||
pub fn get_current_epoch_info() -> EpochData<T> {
|
||||
EpochInfo::<T>::get()
|
||||
}
|
||||
|
||||
/// Return reward pool information for specific epoch
|
||||
pub fn get_epoch_reward_pool(epoch_index: u32) -> Option<EpochRewardPool<T>> {
|
||||
EpochRewardPools::<T>::get(epoch_index)
|
||||
}
|
||||
|
||||
/// Return user's trust score for specific epoch
|
||||
pub fn get_user_trust_score_for_epoch(
|
||||
epoch_index: u32,
|
||||
who: &T::AccountId,
|
||||
) -> Option<u128> {
|
||||
UserEpochScores::<T>::get(epoch_index, who)
|
||||
}
|
||||
|
||||
/// Return reward amount claimed by user from specific epoch
|
||||
pub fn get_claimed_reward(epoch_index: u32, who: &T::AccountId) -> Option<BalanceOf<T>> {
|
||||
ClaimedRewards::<T>::get(epoch_index, who)
|
||||
}
|
||||
|
||||
/// Distribute rewards to parliamentary NFT holders automatically
|
||||
pub fn distribute_parliamentary_rewards(
|
||||
epoch: u32,
|
||||
total_incentive_pool: BalanceOf<T>,
|
||||
) -> DispatchResult {
|
||||
let parliamentary_allocation =
|
||||
total_incentive_pool * PARLIAMENTARY_REWARD_PERCENT.into() / 100u32.into();
|
||||
let per_nft_reward = parliamentary_allocation / PARLIAMENTARY_NFT_COUNT.into();
|
||||
|
||||
let incentive_pot = Self::incentive_pot_account_id();
|
||||
|
||||
for nft_id in 1..=PARLIAMENTARY_NFT_COUNT {
|
||||
if let Some(owner) = Self::get_parliamentary_nft_owner(nft_id) {
|
||||
T::Assets::transfer(
|
||||
T::PezAssetId::get(),
|
||||
&incentive_pot,
|
||||
&owner,
|
||||
per_nft_reward,
|
||||
Preservation::Expendable, /* Allow source account to be deleted even if
|
||||
* it has no tokens during fund transfer */
|
||||
)?;
|
||||
|
||||
Self::deposit_event(Event::ParliamentaryNftRewardDistributed {
|
||||
nft_id,
|
||||
owner,
|
||||
amount: per_nft_reward,
|
||||
epoch,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get parliamentary NFT owner from our storage
|
||||
pub fn get_parliamentary_nft_owner(nft_id: u32) -> Option<T::AccountId> {
|
||||
ParliamentaryNftOwners::<T>::get(nft_id)
|
||||
}
|
||||
|
||||
/// Register parliamentary NFT owner (can be called by governance)
|
||||
pub fn do_register_parliamentary_nft_owner(nft_id: u32, owner: T::AccountId) {
|
||||
ParliamentaryNftOwners::<T>::insert(nft_id, owner.clone());
|
||||
|
||||
// NEW: Emit event
|
||||
Self::deposit_event(Event::ParliamentaryOwnerRegistered { nft_id, owner });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,360 @@
|
||||
// pezkuwi/pallets/pez-rewards/src/mock.rs (v1.0 - dev_accounts FIXED)
|
||||
|
||||
use crate as pezpallet_pez_rewards;
|
||||
use pezframe_support::{
|
||||
assert_ok, construct_runtime, parameter_types,
|
||||
traits::{
|
||||
fungibles::Mutate, AsEnsureOriginWithArg, ConstU128, ConstU32, ConstU64, OnFinalize,
|
||||
OnInitialize,
|
||||
},
|
||||
PalletId,
|
||||
};
|
||||
use pezframe_system::{EnsureRoot, EnsureSigned};
|
||||
use pezsp_core::H256;
|
||||
use pezsp_runtime::{
|
||||
traits::{BlakeTwo256, IdentityLookup},
|
||||
BuildStorage,
|
||||
};
|
||||
|
||||
// --- Dummy Trait Implementations for pezpallet-trust ---
|
||||
pub struct MockStakingScoreProvider;
|
||||
impl pezpallet_trust::StakingScoreProvider<H256, u64> for MockStakingScoreProvider {
|
||||
fn get_staking_score(_who: &H256) -> (u32, u64) {
|
||||
(0, 0)
|
||||
}
|
||||
}
|
||||
pub struct MockReferralScoreProvider;
|
||||
impl pezpallet_trust::ReferralScoreProvider<H256> for MockReferralScoreProvider {
|
||||
fn get_referral_score(_who: &H256) -> u32 {
|
||||
0
|
||||
}
|
||||
}
|
||||
pub struct MockPerwerdeScoreProvider;
|
||||
impl pezpallet_trust::PerwerdeScoreProvider<H256> for MockPerwerdeScoreProvider {
|
||||
fn get_perwerde_score(_who: &H256) -> u32 {
|
||||
0
|
||||
}
|
||||
}
|
||||
pub struct MockTikiScoreProvider;
|
||||
impl pezpallet_trust::TikiScoreProvider<H256> for MockTikiScoreProvider {
|
||||
fn get_tiki_score(_who: &H256) -> u32 {
|
||||
0
|
||||
}
|
||||
}
|
||||
pub struct MockCitizenshipStatusProvider;
|
||||
impl pezpallet_trust::CitizenshipStatusProvider<H256> for MockCitizenshipStatusProvider {
|
||||
fn is_citizen(_who: &H256) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
type Block = pezframe_system::mocking::MockBlock<Test>;
|
||||
type Balance = u128;
|
||||
type BlockNumber = u64;
|
||||
type Weight = pezframe_support::weights::Weight;
|
||||
|
||||
// Configure runtime
|
||||
construct_runtime!(
|
||||
pub enum Test
|
||||
{
|
||||
System: pezframe_system,
|
||||
Balances: pezpallet_balances,
|
||||
Assets: pezpallet_assets,
|
||||
IdentityKyc: pezpallet_identity_kyc,
|
||||
Trust: pezpallet_trust,
|
||||
PezRewards: pezpallet_pez_rewards,
|
||||
}
|
||||
);
|
||||
|
||||
// --- pezframe_system::Config ---
|
||||
parameter_types! {
|
||||
pub const BlockHashCount: BlockNumber = 250;
|
||||
pub const SS58Prefix: u8 = 42;
|
||||
}
|
||||
impl pezframe_system::Config for Test {
|
||||
type BaseCallFilter = pezframe_support::traits::Everything;
|
||||
type BlockWeights = ();
|
||||
type BlockLength = ();
|
||||
type DbWeight = ();
|
||||
type RuntimeOrigin = RuntimeOrigin;
|
||||
type RuntimeCall = RuntimeCall;
|
||||
type Nonce = u64;
|
||||
type Hash = H256;
|
||||
type Hashing = BlakeTwo256;
|
||||
type AccountId = H256;
|
||||
type Lookup = IdentityLookup<Self::AccountId>;
|
||||
type Block = Block;
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type BlockHashCount = BlockHashCount;
|
||||
type Version = ();
|
||||
type PalletInfo = PalletInfo;
|
||||
type AccountData = pezpallet_balances::AccountData<Balance>;
|
||||
type OnNewAccount = ();
|
||||
type OnKilledAccount = ();
|
||||
type SystemWeightInfo = ();
|
||||
type SS58Prefix = SS58Prefix;
|
||||
type OnSetCode = ();
|
||||
type MaxConsumers = ConstU32<16>;
|
||||
type RuntimeTask = ();
|
||||
type SingleBlockMigrations = ();
|
||||
type MultiBlockMigrator = ();
|
||||
type PreInherents = ();
|
||||
type PostInherents = ();
|
||||
type PostTransactions = ();
|
||||
type ExtensionsWeightInfo = ();
|
||||
}
|
||||
|
||||
// --- pezpallet_balances::Config ---
|
||||
parameter_types! {
|
||||
pub const ExistentialDeposit: Balance = 1;
|
||||
pub const MaxLocks: u32 = 50;
|
||||
pub const MaxReserves: u32 = 50;
|
||||
}
|
||||
impl pezpallet_balances::Config for Test {
|
||||
type MaxLocks = MaxLocks;
|
||||
type MaxReserves = MaxReserves;
|
||||
type ReserveIdentifier = [u8; 8];
|
||||
type Balance = Balance;
|
||||
type DustRemoval = ();
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type ExistentialDeposit = ExistentialDeposit;
|
||||
type AccountStore = System;
|
||||
type WeightInfo = ();
|
||||
type RuntimeHoldReason = ();
|
||||
type RuntimeFreezeReason = ();
|
||||
type FreezeIdentifier = ();
|
||||
type MaxFreezes = ();
|
||||
type DoneSlashHandler = ();
|
||||
}
|
||||
|
||||
// --- pezpallet_assets::Config ---
|
||||
parameter_types! {
|
||||
pub const AssetDeposit: Balance = 100;
|
||||
pub const ApprovalDeposit: Balance = 1;
|
||||
pub const StringLimit: u32 = 50;
|
||||
pub const MetadataDepositBase: Balance = 10;
|
||||
pub const MetadataDepositPerByte: Balance = 1;
|
||||
pub const AssetAccountDeposit: Balance = 1;
|
||||
}
|
||||
impl pezpallet_assets::Config for Test {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type Balance = Balance;
|
||||
type AssetId = u32;
|
||||
type AssetIdParameter = u32;
|
||||
type Currency = Balances;
|
||||
type CreateOrigin = AsEnsureOriginWithArg<EnsureSigned<Self::AccountId>>;
|
||||
type ForceOrigin = EnsureRoot<Self::AccountId>;
|
||||
type AssetDeposit = AssetDeposit;
|
||||
type AssetAccountDeposit = AssetAccountDeposit;
|
||||
type MetadataDepositBase = MetadataDepositBase;
|
||||
type MetadataDepositPerByte = MetadataDepositPerByte;
|
||||
type ApprovalDeposit = ApprovalDeposit;
|
||||
type StringLimit = StringLimit;
|
||||
type Freezer = ();
|
||||
type Extra = ();
|
||||
type CallbackHandle = ();
|
||||
type WeightInfo = ();
|
||||
type RemoveItemsLimit = ConstU32<1000>;
|
||||
type Holder = ();
|
||||
type ReserveData = ();
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
type BenchmarkHelper = ();
|
||||
}
|
||||
|
||||
// --- pezpallet_identity_kyc::Config ---
|
||||
pub struct NoOpOnKycApproved;
|
||||
impl pezpallet_identity_kyc::types::OnKycApproved<H256> for NoOpOnKycApproved {
|
||||
fn on_kyc_approved(_who: &H256, _referrer: &H256) {}
|
||||
}
|
||||
|
||||
pub struct NoOpOnCitizenshipRevoked;
|
||||
impl pezpallet_identity_kyc::types::OnCitizenshipRevoked<H256> for NoOpOnCitizenshipRevoked {
|
||||
fn on_citizenship_revoked(_who: &H256) {}
|
||||
}
|
||||
|
||||
pub struct NoOpCitizenNftProvider;
|
||||
impl pezpallet_identity_kyc::types::CitizenNftProvider<H256> for NoOpCitizenNftProvider {
|
||||
fn mint_citizen_nft(_who: &H256) -> Result<(), pezsp_runtime::DispatchError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn mint_citizen_nft_confirmed(_who: &H256) -> Result<(), pezsp_runtime::DispatchError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn burn_citizen_nft(_who: &H256) -> Result<(), pezsp_runtime::DispatchError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const KycApplicationDeposit: Balance = 100;
|
||||
pub const MaxStringLength: u32 = 128;
|
||||
pub const MaxCidLength: u32 = 128;
|
||||
}
|
||||
|
||||
impl pezpallet_identity_kyc::Config for Test {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type Currency = Balances;
|
||||
type GovernanceOrigin = EnsureRoot<H256>;
|
||||
type WeightInfo = ();
|
||||
type OnKycApproved = NoOpOnKycApproved;
|
||||
type OnCitizenshipRevoked = NoOpOnCitizenshipRevoked;
|
||||
type CitizenNftProvider = NoOpCitizenNftProvider;
|
||||
type KycApplicationDeposit = KycApplicationDeposit;
|
||||
type MaxStringLength = MaxStringLength;
|
||||
type MaxCidLength = MaxCidLength;
|
||||
}
|
||||
|
||||
// --- pezpallet_trust::Config ---
|
||||
pub struct MockTrustScore;
|
||||
impl pezpallet_trust::TrustScoreProvider<H256> for MockTrustScore {
|
||||
fn trust_score_of(account: &H256) -> u128 {
|
||||
if *account == alice() {
|
||||
100
|
||||
} else if *account == bob() {
|
||||
50
|
||||
} else if *account == charlie() {
|
||||
75
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
parameter_types! {
|
||||
pub const MaxBatchSize: u32 = 100;
|
||||
}
|
||||
|
||||
impl pezpallet_trust::Config for Test {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type WeightInfo = ();
|
||||
type Score = u128;
|
||||
type ScoreMultiplierBase = ConstU128<1>;
|
||||
type UpdateInterval = ConstU64<100>;
|
||||
type MaxBatchSize = MaxBatchSize;
|
||||
type StakingScoreSource = MockStakingScoreProvider;
|
||||
type ReferralScoreSource = MockReferralScoreProvider;
|
||||
type PerwerdeScoreSource = MockPerwerdeScoreProvider;
|
||||
type TikiScoreSource = MockTikiScoreProvider;
|
||||
type CitizenshipSource = MockCitizenshipStatusProvider;
|
||||
}
|
||||
|
||||
// --- pezpallet_pez_rewards::Config ---
|
||||
parameter_types! {
|
||||
pub const IncentivePotId: PalletId = PalletId(*b"pez/rpot");
|
||||
pub const PezAssetId: u32 = 1;
|
||||
pub ClawbackRecipient: H256 = H256::from_low_u64_be(999);
|
||||
}
|
||||
pub struct MockWeightInfo;
|
||||
impl crate::weights::WeightInfo for MockWeightInfo {
|
||||
fn initialize_rewards_system() -> Weight {
|
||||
Weight::zero()
|
||||
}
|
||||
fn record_trust_score() -> Weight {
|
||||
Weight::zero()
|
||||
}
|
||||
fn finalize_epoch() -> Weight {
|
||||
Weight::zero()
|
||||
}
|
||||
fn claim_reward() -> Weight {
|
||||
Weight::zero()
|
||||
}
|
||||
fn close_epoch() -> Weight {
|
||||
Weight::zero()
|
||||
}
|
||||
fn register_parliamentary_nft_owner() -> Weight {
|
||||
Weight::zero()
|
||||
}
|
||||
}
|
||||
impl pezpallet_pez_rewards::Config for Test {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type Assets = Assets;
|
||||
type TrustScoreSource = MockTrustScore;
|
||||
type IncentivePotId = IncentivePotId;
|
||||
type PezAssetId = PezAssetId;
|
||||
type ClawbackRecipient = ClawbackRecipient;
|
||||
type WeightInfo = MockWeightInfo;
|
||||
type ForceOrigin = EnsureRoot<Self::AccountId>;
|
||||
type CollectionId = u32;
|
||||
type ItemId = u32;
|
||||
}
|
||||
|
||||
// --- Helper Fonksiyonlar ---
|
||||
pub fn alice() -> H256 {
|
||||
H256::from_low_u64_be(1)
|
||||
}
|
||||
pub fn bob() -> H256 {
|
||||
H256::from_low_u64_be(2)
|
||||
}
|
||||
pub fn charlie() -> H256 {
|
||||
H256::from_low_u64_be(3)
|
||||
}
|
||||
pub fn dave() -> H256 {
|
||||
H256::from_low_u64_be(4)
|
||||
}
|
||||
|
||||
// --- new_test_ext ---
|
||||
pub fn new_test_ext() -> pezsp_io::TestExternalities {
|
||||
let mut t = pezframe_system::GenesisConfig::<Test>::default().build_storage().unwrap();
|
||||
|
||||
// BUG FIX: dev_accounts field added (Option type)
|
||||
pezpallet_balances::GenesisConfig::<Test> {
|
||||
balances: vec![
|
||||
(alice(), 1_000_000_000_000_000),
|
||||
(bob(), 1_000_000_000_000_000),
|
||||
(charlie(), 1_000_000_000_000_000),
|
||||
(dave(), 1_000_000_000_000_000),
|
||||
(ClawbackRecipient::get(), 1_000_000_000_000_000),
|
||||
],
|
||||
dev_accounts: None, // No need for dev account in test environment
|
||||
}
|
||||
.assimilate_storage(&mut t)
|
||||
.unwrap();
|
||||
|
||||
pezpallet_assets::GenesisConfig::<Test> {
|
||||
assets: vec![(PezAssetId::get(), alice(), true, 1)],
|
||||
metadata: vec![(PezAssetId::get(), b"Pez Token".to_vec(), b"PEZ".to_vec(), 12)],
|
||||
accounts: vec![(
|
||||
PezAssetId::get(),
|
||||
PezRewards::incentive_pot_account_id(),
|
||||
1_000_000_000_000_000,
|
||||
)],
|
||||
reserves: vec![],
|
||||
next_asset_id: Some(PezAssetId::get() + 1),
|
||||
}
|
||||
.assimilate_storage(&mut t)
|
||||
.unwrap();
|
||||
|
||||
let mut ext = pezsp_io::TestExternalities::new(t);
|
||||
ext.execute_with(|| {
|
||||
System::set_block_number(1);
|
||||
assert_ok!(PezRewards::initialize_rewards_system(RuntimeOrigin::root()));
|
||||
});
|
||||
ext
|
||||
}
|
||||
|
||||
// --- Block Advancement Helper ---
|
||||
pub fn advance_blocks(n: BlockNumber) {
|
||||
let target = System::block_number() + n;
|
||||
while System::block_number() < target {
|
||||
if System::block_number() > 0 {
|
||||
AllPalletsWithSystem::on_finalize(System::block_number());
|
||||
}
|
||||
System::set_block_number(System::block_number() + 1);
|
||||
AllPalletsWithSystem::on_initialize(System::block_number());
|
||||
}
|
||||
}
|
||||
|
||||
// --- Other Helper Functions ---
|
||||
pub fn pez_balance(account: &H256) -> Balance {
|
||||
Assets::balance(PezAssetId::get(), account)
|
||||
}
|
||||
|
||||
pub fn fund_incentive_pot(amount: Balance) {
|
||||
let pot = PezRewards::incentive_pot_account_id();
|
||||
assert_ok!(Assets::mint_into(PezAssetId::get(), &pot, amount));
|
||||
}
|
||||
|
||||
pub fn register_nft_owner(nft_id: u32, owner: H256) {
|
||||
PezRewards::do_register_parliamentary_nft_owner(nft_id, owner);
|
||||
}
|
||||
@@ -0,0 +1,731 @@
|
||||
// tests.rs (v11 - Final Bug Fixes)
|
||||
|
||||
use crate::{mock::*, EpochState, Error, Event};
|
||||
use pezframe_support::{
|
||||
assert_noop, assert_ok,
|
||||
traits::{
|
||||
fungibles::Mutate,
|
||||
tokens::{Fortitude, Precision, Preservation},
|
||||
},
|
||||
};
|
||||
use pezsp_runtime::traits::BadOrigin;
|
||||
|
||||
// =============================================================================
|
||||
// 1. INITIALIZATION TESTS
|
||||
// =============================================================================
|
||||
|
||||
#[test]
|
||||
fn initialize_rewards_system_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let epoch_info = PezRewards::get_current_epoch_info();
|
||||
assert_eq!(epoch_info.current_epoch, 0);
|
||||
assert_eq!(epoch_info.total_epochs_completed, 0);
|
||||
assert_eq!(epoch_info.epoch_start_block, 1);
|
||||
assert_eq!(PezRewards::epoch_status(0), EpochState::Open);
|
||||
|
||||
// BUG FIX E0599: Matches lib.rs v2
|
||||
System::assert_has_event(Event::NewEpochStarted { epoch_index: 0, start_block: 1 }.into());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cannot_initialize_twice() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_noop!(
|
||||
PezRewards::initialize_rewards_system(RuntimeOrigin::root()),
|
||||
Error::<Test>::AlreadyInitialized // BUG FIX E0599: Matches lib.rs v2
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 2. TRUST SCORE RECORDING TESTS
|
||||
// =============================================================================
|
||||
|
||||
#[test]
|
||||
fn record_trust_score_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(PezRewards::record_trust_score(RuntimeOrigin::signed(alice())));
|
||||
let score = PezRewards::get_user_trust_score_for_epoch(0, &alice());
|
||||
assert_eq!(score, Some(100));
|
||||
|
||||
System::assert_has_event(
|
||||
Event::TrustScoreRecorded { user: alice(), epoch_index: 0, trust_score: 100 }.into(),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_users_can_record_scores() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(PezRewards::record_trust_score(RuntimeOrigin::signed(alice())));
|
||||
assert_ok!(PezRewards::record_trust_score(RuntimeOrigin::signed(bob())));
|
||||
assert_ok!(PezRewards::record_trust_score(RuntimeOrigin::signed(charlie())));
|
||||
|
||||
assert_eq!(PezRewards::get_user_trust_score_for_epoch(0, &alice()), Some(100));
|
||||
assert_eq!(PezRewards::get_user_trust_score_for_epoch(0, &bob()), Some(50));
|
||||
assert_eq!(PezRewards::get_user_trust_score_for_epoch(0, &charlie()), Some(75));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn record_trust_score_twice_updates() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(PezRewards::record_trust_score(RuntimeOrigin::signed(alice())));
|
||||
assert_eq!(PezRewards::get_user_trust_score_for_epoch(0, &alice()), Some(100));
|
||||
|
||||
assert_ok!(PezRewards::record_trust_score(RuntimeOrigin::signed(alice())));
|
||||
assert_eq!(PezRewards::get_user_trust_score_for_epoch(0, &alice()), Some(100));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cannot_record_score_for_closed_epoch() {
|
||||
new_test_ext().execute_with(|| {
|
||||
advance_blocks(crate::BLOCKS_PER_EPOCH as u64);
|
||||
assert_ok!(PezRewards::finalize_epoch(RuntimeOrigin::root()));
|
||||
advance_blocks(crate::CLAIM_PERIOD_BLOCKS as u64 + 1);
|
||||
assert_ok!(PezRewards::close_epoch(RuntimeOrigin::root(), 0));
|
||||
|
||||
// FIX: Dave now registering in epoch 1 (epoch 1 Open)
|
||||
assert_ok!(PezRewards::record_trust_score(RuntimeOrigin::signed(dave())));
|
||||
|
||||
// Dave's score should be recorded in epoch 1
|
||||
assert_eq!(PezRewards::get_user_trust_score_for_epoch(1, &dave()), Some(0));
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 3. EPOCH FINALIZATION TESTS
|
||||
// =============================================================================
|
||||
|
||||
#[test]
|
||||
fn getter_functions_work_correctly() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_eq!(PezRewards::get_claimed_reward(0, &alice()), None);
|
||||
assert_eq!(PezRewards::get_user_trust_score_for_epoch(0, &alice()), None);
|
||||
assert_eq!(PezRewards::get_epoch_reward_pool(0), None);
|
||||
assert_eq!(PezRewards::epoch_status(0), EpochState::Open);
|
||||
|
||||
assert_ok!(PezRewards::record_trust_score(RuntimeOrigin::signed(alice())));
|
||||
assert_eq!(PezRewards::get_user_trust_score_for_epoch(0, &alice()), Some(100));
|
||||
|
||||
advance_blocks(crate::BLOCKS_PER_EPOCH as u64);
|
||||
assert_ok!(PezRewards::finalize_epoch(RuntimeOrigin::root()));
|
||||
assert!(PezRewards::get_epoch_reward_pool(0).is_some());
|
||||
// FIX: Should be ClaimPeriod after finalize
|
||||
assert_eq!(PezRewards::epoch_status(0), EpochState::ClaimPeriod);
|
||||
|
||||
assert_ok!(PezRewards::claim_reward(RuntimeOrigin::signed(alice()), 0));
|
||||
assert!(PezRewards::get_claimed_reward(0, &alice()).is_some());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finalize_epoch_too_early_fails() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(PezRewards::record_trust_score(RuntimeOrigin::signed(alice())));
|
||||
|
||||
advance_blocks(crate::BLOCKS_PER_EPOCH as u64 - 1);
|
||||
assert_noop!(
|
||||
PezRewards::finalize_epoch(RuntimeOrigin::root()),
|
||||
Error::<Test>::EpochNotFinished
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finalize_epoch_calculates_rewards_correctly() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(PezRewards::record_trust_score(RuntimeOrigin::signed(alice()))); // 100
|
||||
assert_ok!(PezRewards::record_trust_score(RuntimeOrigin::signed(bob()))); // 50
|
||||
assert_ok!(PezRewards::record_trust_score(RuntimeOrigin::signed(charlie()))); // 75
|
||||
let total_trust: u128 = 100 + 50 + 75;
|
||||
let expected_deadline = System::block_number() +
|
||||
crate::BLOCKS_PER_EPOCH as u64 +
|
||||
crate::CLAIM_PERIOD_BLOCKS as u64;
|
||||
|
||||
let incentive_pot = PezRewards::incentive_pot_account_id();
|
||||
let initial_pot_balance = pez_balance(&incentive_pot);
|
||||
|
||||
advance_blocks(crate::BLOCKS_PER_EPOCH as u64);
|
||||
assert_ok!(PezRewards::finalize_epoch(RuntimeOrigin::root()));
|
||||
|
||||
let reward_pool = PezRewards::get_epoch_reward_pool(0).unwrap();
|
||||
|
||||
// FIX: Reduced amount after parliamentary reward (90%)
|
||||
let trust_score_pool = initial_pot_balance * 90u128 / 100;
|
||||
|
||||
assert_eq!(reward_pool.total_reward_pool, trust_score_pool);
|
||||
assert_eq!(reward_pool.total_trust_score, total_trust);
|
||||
assert_eq!(reward_pool.participants_count, 3);
|
||||
assert_eq!(reward_pool.reward_per_trust_point, trust_score_pool / total_trust);
|
||||
assert_eq!(
|
||||
reward_pool.claim_deadline,
|
||||
System::block_number() + crate::CLAIM_PERIOD_BLOCKS as u64
|
||||
);
|
||||
|
||||
// FIX: Event'te trust_score_pool (90%) bekle
|
||||
System::assert_has_event(
|
||||
Event::EpochRewardPoolCalculated {
|
||||
epoch_index: 0,
|
||||
total_pool: trust_score_pool,
|
||||
participants_count: 3,
|
||||
total_trust_score: total_trust,
|
||||
claim_deadline: expected_deadline,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
System::assert_has_event(
|
||||
Event::NewEpochStarted {
|
||||
epoch_index: 1,
|
||||
start_block: crate::BLOCKS_PER_EPOCH as u64 + 1,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
// FIX: Finalize sonrası ClaimPeriod
|
||||
assert_eq!(PezRewards::epoch_status(0), EpochState::ClaimPeriod);
|
||||
assert_eq!(PezRewards::epoch_status(1), EpochState::Open);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finalize_epoch_fails_if_already_finalized_or_closed() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(PezRewards::record_trust_score(RuntimeOrigin::signed(alice())));
|
||||
advance_blocks(crate::BLOCKS_PER_EPOCH as u64);
|
||||
assert_ok!(PezRewards::finalize_epoch(RuntimeOrigin::root()));
|
||||
|
||||
// FIX: Second finalize tries to finalize epoch 1 (not finished yet)
|
||||
assert_noop!(
|
||||
PezRewards::finalize_epoch(RuntimeOrigin::root()),
|
||||
Error::<Test>::EpochNotFinished
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finalize_epoch_no_participants() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let incentive_pot = PezRewards::incentive_pot_account_id();
|
||||
let pot_balance_before = pez_balance(&incentive_pot);
|
||||
|
||||
advance_blocks(crate::BLOCKS_PER_EPOCH as u64);
|
||||
assert_ok!(PezRewards::finalize_epoch(RuntimeOrigin::root()));
|
||||
|
||||
let reward_pool = PezRewards::get_epoch_reward_pool(0).unwrap();
|
||||
assert_eq!(reward_pool.total_trust_score, 0);
|
||||
assert_eq!(reward_pool.participants_count, 0);
|
||||
assert_eq!(reward_pool.reward_per_trust_point, 0);
|
||||
|
||||
// FIX: NFT owner not registered, parliamentary reward not distributed
|
||||
// All balance remains in pot (100%)
|
||||
let pot_balance_after = pez_balance(&incentive_pot);
|
||||
assert_eq!(pot_balance_after, pot_balance_before);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finalize_epoch_zero_trust_score_participant() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(PezRewards::record_trust_score(RuntimeOrigin::signed(dave()))); // Skor 0
|
||||
// FIX: Zero scores are now being recorded
|
||||
assert_eq!(PezRewards::get_user_trust_score_for_epoch(0, &dave()), Some(0));
|
||||
|
||||
let incentive_pot = PezRewards::incentive_pot_account_id();
|
||||
let pot_balance_before = pez_balance(&incentive_pot);
|
||||
|
||||
advance_blocks(crate::BLOCKS_PER_EPOCH as u64);
|
||||
assert_ok!(PezRewards::finalize_epoch(RuntimeOrigin::root()));
|
||||
|
||||
let reward_pool = PezRewards::get_epoch_reward_pool(0).unwrap();
|
||||
assert_eq!(reward_pool.total_trust_score, 0);
|
||||
assert_eq!(reward_pool.participants_count, 1);
|
||||
assert_eq!(reward_pool.reward_per_trust_point, 0);
|
||||
|
||||
// FIX: NFT owner not registered, parliamentary reward not distributed
|
||||
// All balance remains in pot (100%)
|
||||
let pot_balance_after = pez_balance(&incentive_pot);
|
||||
assert_eq!(pot_balance_after, pot_balance_before);
|
||||
|
||||
// FIX: NoRewardToClaim instead of NoTrustScoreForEpoch (0 score exists but reward is 0)
|
||||
assert_noop!(
|
||||
PezRewards::claim_reward(RuntimeOrigin::signed(dave()), 0),
|
||||
Error::<Test>::NoRewardToClaim
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 4. CLAIM REWARD TESTS
|
||||
// =============================================================================
|
||||
|
||||
#[test]
|
||||
fn claim_reward_works_for_single_user() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(PezRewards::record_trust_score(RuntimeOrigin::signed(alice()))); // 100
|
||||
advance_blocks(crate::BLOCKS_PER_EPOCH as u64);
|
||||
assert_ok!(PezRewards::finalize_epoch(RuntimeOrigin::root()));
|
||||
|
||||
let balance_before = pez_balance(&alice());
|
||||
let reward_pool = PezRewards::get_epoch_reward_pool(0).unwrap();
|
||||
let expected_reward = reward_pool.reward_per_trust_point * 100;
|
||||
|
||||
assert_ok!(PezRewards::claim_reward(RuntimeOrigin::signed(alice()), 0));
|
||||
|
||||
let balance_after = pez_balance(&alice());
|
||||
assert_eq!(balance_after, balance_before + expected_reward);
|
||||
|
||||
System::assert_last_event(
|
||||
Event::RewardClaimed { user: alice(), epoch_index: 0, amount: expected_reward }.into(),
|
||||
);
|
||||
assert!(PezRewards::get_claimed_reward(0, &alice()).is_some());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn claim_reward_works_for_multiple_users() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(PezRewards::record_trust_score(RuntimeOrigin::signed(alice()))); // 100
|
||||
assert_ok!(PezRewards::record_trust_score(RuntimeOrigin::signed(bob()))); // 50
|
||||
advance_blocks(crate::BLOCKS_PER_EPOCH as u64);
|
||||
assert_ok!(PezRewards::finalize_epoch(RuntimeOrigin::root()));
|
||||
|
||||
let balance1_before = pez_balance(&alice());
|
||||
let balance2_before = pez_balance(&bob());
|
||||
|
||||
let reward_pool = PezRewards::get_epoch_reward_pool(0).unwrap();
|
||||
let reward1 = reward_pool.reward_per_trust_point * 100;
|
||||
let reward2 = reward_pool.reward_per_trust_point * 50;
|
||||
|
||||
assert_ok!(PezRewards::claim_reward(RuntimeOrigin::signed(alice()), 0));
|
||||
assert_ok!(PezRewards::claim_reward(RuntimeOrigin::signed(bob()), 0));
|
||||
|
||||
let balance1_after = pez_balance(&alice());
|
||||
let balance2_after = pez_balance(&bob());
|
||||
|
||||
assert_eq!(balance1_after, balance1_before + reward1);
|
||||
assert_eq!(balance2_after, balance2_before + reward2);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn claim_reward_fails_if_already_claimed() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(PezRewards::record_trust_score(RuntimeOrigin::signed(alice())));
|
||||
advance_blocks(crate::BLOCKS_PER_EPOCH as u64);
|
||||
assert_ok!(PezRewards::finalize_epoch(RuntimeOrigin::root()));
|
||||
assert_ok!(PezRewards::claim_reward(RuntimeOrigin::signed(alice()), 0));
|
||||
|
||||
assert_noop!(
|
||||
PezRewards::claim_reward(RuntimeOrigin::signed(alice()), 0),
|
||||
Error::<Test>::RewardAlreadyClaimed
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn claim_reward_fails_if_not_participant() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(PezRewards::record_trust_score(RuntimeOrigin::signed(alice())));
|
||||
advance_blocks(crate::BLOCKS_PER_EPOCH as u64);
|
||||
assert_ok!(PezRewards::finalize_epoch(RuntimeOrigin::root()));
|
||||
|
||||
// FIX: Bob not registered, should get NoTrustScoreForEpoch error
|
||||
assert_noop!(
|
||||
PezRewards::claim_reward(RuntimeOrigin::signed(bob()), 0),
|
||||
Error::<Test>::NoTrustScoreForEpoch
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn claim_reward_fails_if_epoch_not_finalized() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(PezRewards::record_trust_score(RuntimeOrigin::signed(alice())));
|
||||
// FIX: Unfinalized epoch -> ClaimPeriodExpired error (Open state)
|
||||
assert_noop!(
|
||||
PezRewards::claim_reward(RuntimeOrigin::signed(alice()), 0),
|
||||
Error::<Test>::ClaimPeriodExpired
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn claim_reward_fails_if_claim_period_over() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(PezRewards::record_trust_score(RuntimeOrigin::signed(alice())));
|
||||
advance_blocks(crate::BLOCKS_PER_EPOCH as u64);
|
||||
assert_ok!(PezRewards::finalize_epoch(RuntimeOrigin::root()));
|
||||
|
||||
advance_blocks(crate::CLAIM_PERIOD_BLOCKS as u64 + 1);
|
||||
|
||||
assert_noop!(
|
||||
PezRewards::claim_reward(RuntimeOrigin::signed(alice()), 0),
|
||||
Error::<Test>::ClaimPeriodExpired // BUG FIX E0599
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn claim_reward_fails_if_epoch_closed() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(PezRewards::record_trust_score(RuntimeOrigin::signed(alice())));
|
||||
advance_blocks(crate::BLOCKS_PER_EPOCH as u64);
|
||||
assert_ok!(PezRewards::finalize_epoch(RuntimeOrigin::root()));
|
||||
advance_blocks(crate::CLAIM_PERIOD_BLOCKS as u64 + 1);
|
||||
assert_ok!(PezRewards::close_epoch(RuntimeOrigin::root(), 0));
|
||||
|
||||
// FIX: Epoch Closed -> ClaimPeriodExpired error
|
||||
assert_noop!(
|
||||
PezRewards::claim_reward(RuntimeOrigin::signed(alice()), 0),
|
||||
Error::<Test>::ClaimPeriodExpired
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn claim_reward_fails_if_pot_insufficient_during_claim() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(PezRewards::record_trust_score(RuntimeOrigin::signed(alice())));
|
||||
advance_blocks(crate::BLOCKS_PER_EPOCH as u64);
|
||||
assert_ok!(PezRewards::finalize_epoch(RuntimeOrigin::root()));
|
||||
|
||||
let incentive_pot = PezRewards::incentive_pot_account_id();
|
||||
let pez_pot_balance = pez_balance(&incentive_pot);
|
||||
assert_ok!(Assets::burn_from(
|
||||
PezAssetId::get(),
|
||||
&incentive_pot,
|
||||
pez_pot_balance,
|
||||
Preservation::Expendable,
|
||||
Precision::Exact,
|
||||
Fortitude::Polite
|
||||
));
|
||||
|
||||
// FIX: Arithmetic Underflow error expected
|
||||
assert!(PezRewards::claim_reward(RuntimeOrigin::signed(alice()), 0).is_err());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn claim_reward_fails_for_wrong_epoch() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(PezRewards::record_trust_score(RuntimeOrigin::signed(alice())));
|
||||
advance_blocks(crate::BLOCKS_PER_EPOCH as u64);
|
||||
assert_ok!(PezRewards::finalize_epoch(RuntimeOrigin::root()));
|
||||
|
||||
// FIX: Epoch 1 not yet finalized -> ClaimPeriodExpired
|
||||
assert_noop!(
|
||||
PezRewards::claim_reward(RuntimeOrigin::signed(alice()), 1),
|
||||
Error::<Test>::ClaimPeriodExpired
|
||||
);
|
||||
|
||||
// Epoch 999 yok -> ClaimPeriodExpired
|
||||
assert_noop!(
|
||||
PezRewards::claim_reward(RuntimeOrigin::signed(alice()), 999),
|
||||
Error::<Test>::ClaimPeriodExpired
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 5. CLOSE EPOCH TESTS
|
||||
// =============================================================================
|
||||
|
||||
#[test]
|
||||
fn close_epoch_works_after_claim_period() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(PezRewards::record_trust_score(RuntimeOrigin::signed(alice()))); // Claim etmeyecek
|
||||
assert_ok!(PezRewards::record_trust_score(RuntimeOrigin::signed(bob()))); // Claim edecek
|
||||
|
||||
let incentive_pot = PezRewards::incentive_pot_account_id();
|
||||
let pot_balance_before_finalize = pez_balance(&incentive_pot);
|
||||
|
||||
advance_blocks(crate::BLOCKS_PER_EPOCH as u64);
|
||||
assert_ok!(PezRewards::finalize_epoch(RuntimeOrigin::root()));
|
||||
|
||||
let reward_pool = PezRewards::get_epoch_reward_pool(0).unwrap();
|
||||
let alice_reward = reward_pool.reward_per_trust_point * 100;
|
||||
let bob_reward = reward_pool.reward_per_trust_point * 50;
|
||||
|
||||
assert_ok!(PezRewards::claim_reward(RuntimeOrigin::signed(bob()), 0)); // Bob claim etti
|
||||
|
||||
let clawback_recipient = ClawbackRecipient::get();
|
||||
let balance_before = pez_balance(&clawback_recipient);
|
||||
|
||||
// FIX: Remaining balance in pot = initial - bob's claim
|
||||
// (No NFT owner, parliamentary reward not distributed)
|
||||
let pot_balance_before_close = pez_balance(&incentive_pot);
|
||||
let expected_unclaimed = pot_balance_before_close;
|
||||
|
||||
advance_blocks(crate::CLAIM_PERIOD_BLOCKS as u64 + 1);
|
||||
|
||||
assert_ok!(PezRewards::close_epoch(RuntimeOrigin::root(), 0));
|
||||
|
||||
let balance_after = pez_balance(&clawback_recipient);
|
||||
// FIX: All remaining pot (including alice's reward) should be clawed back
|
||||
assert_eq!(balance_after, balance_before + expected_unclaimed);
|
||||
|
||||
assert_eq!(PezRewards::epoch_status(0), EpochState::Closed);
|
||||
|
||||
System::assert_last_event(
|
||||
Event::EpochClosed {
|
||||
epoch_index: 0,
|
||||
unclaimed_amount: expected_unclaimed,
|
||||
clawback_recipient,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn close_epoch_fails_before_claim_period_ends() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(PezRewards::record_trust_score(RuntimeOrigin::signed(alice())));
|
||||
advance_blocks(crate::BLOCKS_PER_EPOCH as u64);
|
||||
assert_ok!(PezRewards::finalize_epoch(RuntimeOrigin::root()));
|
||||
|
||||
advance_blocks(crate::CLAIM_PERIOD_BLOCKS as u64 - 1);
|
||||
assert_noop!(
|
||||
PezRewards::close_epoch(RuntimeOrigin::root(), 0),
|
||||
Error::<Test>::ClaimPeriodExpired // BUG FIX E0599
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn close_epoch_fails_if_already_closed() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(PezRewards::record_trust_score(RuntimeOrigin::signed(alice())));
|
||||
advance_blocks(crate::BLOCKS_PER_EPOCH as u64);
|
||||
assert_ok!(PezRewards::finalize_epoch(RuntimeOrigin::root()));
|
||||
advance_blocks(crate::CLAIM_PERIOD_BLOCKS as u64 + 1);
|
||||
assert_ok!(PezRewards::close_epoch(RuntimeOrigin::root(), 0));
|
||||
|
||||
assert_noop!(
|
||||
PezRewards::close_epoch(RuntimeOrigin::root(), 0),
|
||||
Error::<Test>::EpochAlreadyClosed
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn close_epoch_fails_if_not_finalized() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(PezRewards::record_trust_score(RuntimeOrigin::signed(alice())));
|
||||
advance_blocks(crate::CLAIM_PERIOD_BLOCKS as u64 + 1);
|
||||
assert_noop!(
|
||||
PezRewards::close_epoch(RuntimeOrigin::root(), 0),
|
||||
Error::<Test>::EpochAlreadyClosed // This error returns even if not finalized
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 6. PARLIAMENTARY REWARDS TESTS
|
||||
// =============================================================================
|
||||
|
||||
#[test]
|
||||
fn parliamentary_rewards_distributed_correctly() {
|
||||
new_test_ext().execute_with(|| {
|
||||
register_nft_owner(1, dave());
|
||||
register_nft_owner(2, alice());
|
||||
assert_ok!(PezRewards::record_trust_score(RuntimeOrigin::signed(alice()))); // 100
|
||||
|
||||
let incentive_pot = PezRewards::incentive_pot_account_id();
|
||||
let pot_balance = pez_balance(&incentive_pot);
|
||||
|
||||
let expected_parliamentary_reward_pot =
|
||||
pot_balance * u128::from(crate::PARLIAMENTARY_REWARD_PERCENT) / 100;
|
||||
let expected_parliamentary_reward =
|
||||
expected_parliamentary_reward_pot / u128::from(crate::PARLIAMENTARY_NFT_COUNT);
|
||||
|
||||
let dave_balance_before = pez_balance(&dave());
|
||||
let alice_balance_before = pez_balance(&alice());
|
||||
|
||||
advance_blocks(crate::BLOCKS_PER_EPOCH as u64);
|
||||
assert_ok!(PezRewards::finalize_epoch(RuntimeOrigin::root()));
|
||||
|
||||
let dave_balance_after = pez_balance(&dave());
|
||||
assert_eq!(dave_balance_after, dave_balance_before + expected_parliamentary_reward);
|
||||
|
||||
let reward_pool = PezRewards::get_epoch_reward_pool(0).unwrap();
|
||||
let trust_reward = reward_pool.reward_per_trust_point * 100;
|
||||
|
||||
let alice_balance_after_finalize = pez_balance(&alice());
|
||||
assert_eq!(
|
||||
alice_balance_after_finalize,
|
||||
alice_balance_before + expected_parliamentary_reward
|
||||
);
|
||||
|
||||
assert_ok!(PezRewards::claim_reward(RuntimeOrigin::signed(alice()), 0));
|
||||
let alice_balance_after_claim = pez_balance(&alice());
|
||||
assert_eq!(alice_balance_after_claim, alice_balance_after_finalize + trust_reward);
|
||||
|
||||
System::assert_has_event(
|
||||
Event::ParliamentaryNftRewardDistributed {
|
||||
nft_id: 1,
|
||||
owner: dave(),
|
||||
amount: expected_parliamentary_reward,
|
||||
epoch: 0,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
System::assert_has_event(
|
||||
Event::ParliamentaryNftRewardDistributed {
|
||||
nft_id: 2,
|
||||
owner: alice(),
|
||||
amount: expected_parliamentary_reward,
|
||||
epoch: 0,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parliamentary_reward_division_precision() {
|
||||
new_test_ext().execute_with(|| {
|
||||
register_nft_owner(1, dave());
|
||||
register_nft_owner(2, alice());
|
||||
|
||||
let incentive_pot = PezRewards::incentive_pot_account_id();
|
||||
let current_balance = pez_balance(&incentive_pot);
|
||||
assert_ok!(Assets::burn_from(
|
||||
PezAssetId::get(),
|
||||
&incentive_pot,
|
||||
current_balance,
|
||||
Preservation::Expendable,
|
||||
Precision::Exact,
|
||||
Fortitude::Polite
|
||||
));
|
||||
|
||||
// FIX: Put larger amount (to avoid BelowMinimum error)
|
||||
fund_incentive_pot(100_000);
|
||||
|
||||
let dave_balance_before = pez_balance(&dave());
|
||||
advance_blocks(crate::BLOCKS_PER_EPOCH as u64);
|
||||
assert_ok!(PezRewards::finalize_epoch(RuntimeOrigin::root()));
|
||||
|
||||
let dave_balance_after = pez_balance(&dave());
|
||||
// 10% of 100_000 = 10_000 / 201 NFT = 49 per NFT
|
||||
let expected_reward = 49;
|
||||
assert_eq!(dave_balance_after, dave_balance_before + expected_reward);
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 7. NFT OWNER REGISTRATION TESTS
|
||||
// =============================================================================
|
||||
|
||||
#[test]
|
||||
fn register_parliamentary_nft_owner_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_eq!(PezRewards::get_parliamentary_nft_owner(10), None);
|
||||
assert_ok!(PezRewards::register_parliamentary_nft_owner(
|
||||
RuntimeOrigin::root(),
|
||||
10,
|
||||
alice()
|
||||
));
|
||||
assert_eq!(PezRewards::get_parliamentary_nft_owner(10), Some(alice()));
|
||||
|
||||
System::assert_last_event(
|
||||
Event::ParliamentaryOwnerRegistered { nft_id: 10, owner: alice() }.into(),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn register_parliamentary_nft_owner_fails_for_non_root() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_noop!(
|
||||
PezRewards::register_parliamentary_nft_owner(
|
||||
RuntimeOrigin::signed(alice()),
|
||||
10,
|
||||
alice()
|
||||
),
|
||||
BadOrigin
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn register_parliamentary_nft_owner_updates_existing() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(PezRewards::register_parliamentary_nft_owner(
|
||||
RuntimeOrigin::root(),
|
||||
10,
|
||||
alice()
|
||||
));
|
||||
assert_eq!(PezRewards::get_parliamentary_nft_owner(10), Some(alice()));
|
||||
|
||||
assert_ok!(PezRewards::register_parliamentary_nft_owner(RuntimeOrigin::root(), 10, bob()));
|
||||
assert_eq!(PezRewards::get_parliamentary_nft_owner(10), Some(bob()));
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 8. MULTIPLE EPOCHS TEST
|
||||
// =============================================================================
|
||||
|
||||
#[test]
|
||||
fn multiple_epochs_work_correctly() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// --- EPOCH 0 ---
|
||||
assert_ok!(PezRewards::record_trust_score(RuntimeOrigin::signed(alice()))); // 100
|
||||
assert_ok!(PezRewards::record_trust_score(RuntimeOrigin::signed(bob()))); // 50
|
||||
advance_blocks(crate::BLOCKS_PER_EPOCH as u64);
|
||||
assert_ok!(PezRewards::finalize_epoch(RuntimeOrigin::root()));
|
||||
|
||||
let reward_pool_0 = PezRewards::get_epoch_reward_pool(0).unwrap();
|
||||
let reward1_0 = reward_pool_0.reward_per_trust_point * 100;
|
||||
let reward2_0 = reward_pool_0.reward_per_trust_point * 50;
|
||||
assert_ok!(PezRewards::claim_reward(RuntimeOrigin::signed(alice()), 0));
|
||||
assert_ok!(PezRewards::claim_reward(RuntimeOrigin::signed(bob()), 0));
|
||||
|
||||
// --- EPOCH 1 ---
|
||||
assert_eq!(PezRewards::get_current_epoch_info().current_epoch, 1);
|
||||
|
||||
fund_incentive_pot(1_000_000_000_000_000);
|
||||
|
||||
assert_ok!(PezRewards::record_trust_score(RuntimeOrigin::signed(alice()))); // 100 (Epoch 1 için)
|
||||
advance_blocks(crate::BLOCKS_PER_EPOCH as u64);
|
||||
assert_ok!(PezRewards::finalize_epoch(RuntimeOrigin::root())); // Epoch 1'i finalize et
|
||||
|
||||
let reward_pool_1 = PezRewards::get_epoch_reward_pool(1).unwrap(); // Epoch 1 havuzu
|
||||
let reward1_1 = reward_pool_1.reward_per_trust_point * 100;
|
||||
assert_ok!(PezRewards::claim_reward(RuntimeOrigin::signed(alice()), 1)); // Epoch 1'den claim et
|
||||
|
||||
// Check balances
|
||||
let alice_balance = pez_balance(&alice());
|
||||
let bob_balance = pez_balance(&bob());
|
||||
assert_eq!(alice_balance, reward1_0 + reward1_1);
|
||||
assert_eq!(bob_balance, reward2_0);
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 9. ORIGIN CHECKS
|
||||
// =============================================================================
|
||||
|
||||
#[test]
|
||||
fn non_root_origin_fails_for_privileged_calls() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_noop!(
|
||||
PezRewards::initialize_rewards_system(RuntimeOrigin::signed(alice())),
|
||||
BadOrigin
|
||||
);
|
||||
assert_noop!(
|
||||
PezRewards::register_parliamentary_nft_owner(RuntimeOrigin::signed(alice()), 1, bob()),
|
||||
BadOrigin
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_signed_origin_fails_for_user_calls() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_noop!(PezRewards::record_trust_score(RuntimeOrigin::root()), BadOrigin);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
|
||||
//! Autogenerated weights for `pezpallet_pez_rewards`
|
||||
//!
|
||||
//! THIS FILE WAS AUTO-GENERATED USING THE BIZINIKIWI BENCHMARK CLI VERSION 32.0.0
|
||||
//! DATE: 2025-12-08, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
|
||||
//! WORST CASE MAP SIZE: `1000000`
|
||||
//! HOSTNAME: `MamostePC`, CPU: `11th Gen Intel(R) Core(TM) i9-11950H @ 2.60GHz`
|
||||
//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: `1024`
|
||||
|
||||
// Executed Command:
|
||||
// ./target/release/frame-omni-bencher
|
||||
// v1
|
||||
// benchmark
|
||||
// pallet
|
||||
// --runtime
|
||||
// target/release/wbuild/people-pezkuwichain-runtime/people_pezkuwichain_runtime.compact.compressed.wasm
|
||||
// --pallets
|
||||
// pezpallet_pez_rewards
|
||||
// -e
|
||||
// all
|
||||
// --steps
|
||||
// 50
|
||||
// --repeat
|
||||
// 20
|
||||
// --output
|
||||
// pezcumulus/teyrchains/pallets/pez-rewards/src/weights.rs
|
||||
// --template
|
||||
// bizinikiwi/.maintain/frame-weight-template.hbs
|
||||
|
||||
#![cfg_attr(rustfmt, rustfmt_skip)]
|
||||
#![allow(unused_parens)]
|
||||
#![allow(unused_imports)]
|
||||
#![allow(missing_docs)]
|
||||
#![allow(dead_code)]
|
||||
|
||||
use pezframe_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}};
|
||||
use core::marker::PhantomData;
|
||||
|
||||
/// Weight functions needed for `pezpallet_pez_rewards`.
|
||||
pub trait WeightInfo {
|
||||
fn initialize_rewards_system() -> Weight;
|
||||
fn record_trust_score() -> Weight;
|
||||
fn finalize_epoch() -> Weight;
|
||||
fn claim_reward() -> Weight;
|
||||
fn close_epoch() -> Weight;
|
||||
fn register_parliamentary_nft_owner() -> Weight;
|
||||
}
|
||||
|
||||
/// Weights for `pezpallet_pez_rewards` using the Bizinikiwi node and recommended hardware.
|
||||
pub struct BizinikiwiWeight<T>(PhantomData<T>);
|
||||
impl<T: pezframe_system::Config> WeightInfo for BizinikiwiWeight<T> {
|
||||
/// Storage: `PezRewards::EpochInfo` (r:1 w:1)
|
||||
/// Proof: `PezRewards::EpochInfo` (`max_values`: Some(1), `max_size`: Some(12), added: 507, mode: `MaxEncodedLen`)
|
||||
/// Storage: `PezRewards::EpochStatus` (r:0 w:1)
|
||||
/// Proof: `PezRewards::EpochStatus` (`max_values`: None, `max_size`: Some(21), added: 2496, mode: `MaxEncodedLen`)
|
||||
fn initialize_rewards_system() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `42`
|
||||
// Estimated: `1497`
|
||||
// Minimum execution time: 16_071_000 picoseconds.
|
||||
Weight::from_parts(17_129_000, 1497)
|
||||
.saturating_add(T::DbWeight::get().reads(1_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(2_u64))
|
||||
}
|
||||
/// Storage: `PezRewards::EpochInfo` (r:1 w:0)
|
||||
/// Proof: `PezRewards::EpochInfo` (`max_values`: Some(1), `max_size`: Some(12), added: 507, mode: `MaxEncodedLen`)
|
||||
/// Storage: `PezRewards::EpochStatus` (r:1 w:0)
|
||||
/// Proof: `PezRewards::EpochStatus` (`max_values`: None, `max_size`: Some(21), added: 2496, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Trust::TrustScores` (r:1 w:0)
|
||||
/// Proof: `Trust::TrustScores` (`max_values`: None, `max_size`: Some(64), added: 2539, mode: `MaxEncodedLen`)
|
||||
/// Storage: `PezRewards::UserEpochScores` (r:0 w:1)
|
||||
/// Proof: `PezRewards::UserEpochScores` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
|
||||
fn record_trust_score() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `124`
|
||||
// Estimated: `3529`
|
||||
// Minimum execution time: 25_152_000 picoseconds.
|
||||
Weight::from_parts(27_935_000, 3529)
|
||||
.saturating_add(T::DbWeight::get().reads(3_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(1_u64))
|
||||
}
|
||||
/// Storage: `PezRewards::EpochInfo` (r:1 w:1)
|
||||
/// Proof: `PezRewards::EpochInfo` (`max_values`: Some(1), `max_size`: Some(12), added: 507, mode: `MaxEncodedLen`)
|
||||
/// Storage: `PezRewards::EpochStatus` (r:1 w:2)
|
||||
/// Proof: `PezRewards::EpochStatus` (`max_values`: None, `max_size`: Some(21), added: 2496, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Assets::Account` (r:1 w:0)
|
||||
/// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
|
||||
/// Storage: `PezRewards::ParliamentaryNftOwners` (r:201 w:0)
|
||||
/// Proof: `PezRewards::ParliamentaryNftOwners` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`)
|
||||
/// Storage: `PezRewards::UserEpochScores` (r:1 w:0)
|
||||
/// Proof: `PezRewards::UserEpochScores` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
|
||||
/// Storage: `PezRewards::EpochRewardPools` (r:0 w:1)
|
||||
/// Proof: `PezRewards::EpochRewardPools` (`max_values`: None, `max_size`: Some(80), added: 2555, mode: `MaxEncodedLen`)
|
||||
fn finalize_epoch() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `342`
|
||||
// Estimated: `508917`
|
||||
// Minimum execution time: 355_960_000 picoseconds.
|
||||
Weight::from_parts(383_120_000, 508917)
|
||||
.saturating_add(T::DbWeight::get().reads(205_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(4_u64))
|
||||
}
|
||||
/// Storage: `PezRewards::EpochStatus` (r:1 w:0)
|
||||
/// Proof: `PezRewards::EpochStatus` (`max_values`: None, `max_size`: Some(21), added: 2496, mode: `MaxEncodedLen`)
|
||||
/// Storage: `PezRewards::ClaimedRewards` (r:1 w:1)
|
||||
/// Proof: `PezRewards::ClaimedRewards` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
|
||||
/// Storage: `PezRewards::EpochRewardPools` (r:1 w:0)
|
||||
/// Proof: `PezRewards::EpochRewardPools` (`max_values`: None, `max_size`: Some(80), added: 2555, mode: `MaxEncodedLen`)
|
||||
/// Storage: `PezRewards::UserEpochScores` (r:1 w:0)
|
||||
/// Proof: `PezRewards::UserEpochScores` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Assets::Asset` (r:1 w:1)
|
||||
/// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Assets::Account` (r:2 w:2)
|
||||
/// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
|
||||
fn claim_reward() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `797`
|
||||
// Estimated: `6208`
|
||||
// Minimum execution time: 64_577_000 picoseconds.
|
||||
Weight::from_parts(67_351_000, 6208)
|
||||
.saturating_add(T::DbWeight::get().reads(7_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(4_u64))
|
||||
}
|
||||
/// Storage: `PezRewards::EpochStatus` (r:1 w:1)
|
||||
/// Proof: `PezRewards::EpochStatus` (`max_values`: None, `max_size`: Some(21), added: 2496, mode: `MaxEncodedLen`)
|
||||
/// Storage: `PezRewards::EpochRewardPools` (r:1 w:0)
|
||||
/// Proof: `PezRewards::EpochRewardPools` (`max_values`: None, `max_size`: Some(80), added: 2555, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Assets::Account` (r:2 w:2)
|
||||
/// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Assets::Asset` (r:1 w:1)
|
||||
/// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
|
||||
/// Storage: `System::Account` (r:2 w:2)
|
||||
/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
|
||||
fn close_epoch() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `767`
|
||||
// Estimated: `6208`
|
||||
// Minimum execution time: 66_570_000 picoseconds.
|
||||
Weight::from_parts(70_662_000, 6208)
|
||||
.saturating_add(T::DbWeight::get().reads(7_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(6_u64))
|
||||
}
|
||||
/// Storage: `PezRewards::ParliamentaryNftOwners` (r:0 w:1)
|
||||
/// Proof: `PezRewards::ParliamentaryNftOwners` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`)
|
||||
fn register_parliamentary_nft_owner() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `0`
|
||||
// Estimated: `0`
|
||||
// Minimum execution time: 7_785_000 picoseconds.
|
||||
Weight::from_parts(7_994_000, 0)
|
||||
.saturating_add(T::DbWeight::get().writes(1_u64))
|
||||
}
|
||||
}
|
||||
|
||||
// For backwards compatibility and tests.
|
||||
impl WeightInfo for () {
|
||||
/// Storage: `PezRewards::EpochInfo` (r:1 w:1)
|
||||
/// Proof: `PezRewards::EpochInfo` (`max_values`: Some(1), `max_size`: Some(12), added: 507, mode: `MaxEncodedLen`)
|
||||
/// Storage: `PezRewards::EpochStatus` (r:0 w:1)
|
||||
/// Proof: `PezRewards::EpochStatus` (`max_values`: None, `max_size`: Some(21), added: 2496, mode: `MaxEncodedLen`)
|
||||
fn initialize_rewards_system() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `42`
|
||||
// Estimated: `1497`
|
||||
// Minimum execution time: 16_071_000 picoseconds.
|
||||
Weight::from_parts(17_129_000, 1497)
|
||||
.saturating_add(RocksDbWeight::get().reads(1_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(2_u64))
|
||||
}
|
||||
/// Storage: `PezRewards::EpochInfo` (r:1 w:0)
|
||||
/// Proof: `PezRewards::EpochInfo` (`max_values`: Some(1), `max_size`: Some(12), added: 507, mode: `MaxEncodedLen`)
|
||||
/// Storage: `PezRewards::EpochStatus` (r:1 w:0)
|
||||
/// Proof: `PezRewards::EpochStatus` (`max_values`: None, `max_size`: Some(21), added: 2496, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Trust::TrustScores` (r:1 w:0)
|
||||
/// Proof: `Trust::TrustScores` (`max_values`: None, `max_size`: Some(64), added: 2539, mode: `MaxEncodedLen`)
|
||||
/// Storage: `PezRewards::UserEpochScores` (r:0 w:1)
|
||||
/// Proof: `PezRewards::UserEpochScores` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
|
||||
fn record_trust_score() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `124`
|
||||
// Estimated: `3529`
|
||||
// Minimum execution time: 25_152_000 picoseconds.
|
||||
Weight::from_parts(27_935_000, 3529)
|
||||
.saturating_add(RocksDbWeight::get().reads(3_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(1_u64))
|
||||
}
|
||||
/// Storage: `PezRewards::EpochInfo` (r:1 w:1)
|
||||
/// Proof: `PezRewards::EpochInfo` (`max_values`: Some(1), `max_size`: Some(12), added: 507, mode: `MaxEncodedLen`)
|
||||
/// Storage: `PezRewards::EpochStatus` (r:1 w:2)
|
||||
/// Proof: `PezRewards::EpochStatus` (`max_values`: None, `max_size`: Some(21), added: 2496, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Assets::Account` (r:1 w:0)
|
||||
/// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
|
||||
/// Storage: `PezRewards::ParliamentaryNftOwners` (r:201 w:0)
|
||||
/// Proof: `PezRewards::ParliamentaryNftOwners` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`)
|
||||
/// Storage: `PezRewards::UserEpochScores` (r:1 w:0)
|
||||
/// Proof: `PezRewards::UserEpochScores` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
|
||||
/// Storage: `PezRewards::EpochRewardPools` (r:0 w:1)
|
||||
/// Proof: `PezRewards::EpochRewardPools` (`max_values`: None, `max_size`: Some(80), added: 2555, mode: `MaxEncodedLen`)
|
||||
fn finalize_epoch() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `342`
|
||||
// Estimated: `508917`
|
||||
// Minimum execution time: 355_960_000 picoseconds.
|
||||
Weight::from_parts(383_120_000, 508917)
|
||||
.saturating_add(RocksDbWeight::get().reads(205_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(4_u64))
|
||||
}
|
||||
/// Storage: `PezRewards::EpochStatus` (r:1 w:0)
|
||||
/// Proof: `PezRewards::EpochStatus` (`max_values`: None, `max_size`: Some(21), added: 2496, mode: `MaxEncodedLen`)
|
||||
/// Storage: `PezRewards::ClaimedRewards` (r:1 w:1)
|
||||
/// Proof: `PezRewards::ClaimedRewards` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
|
||||
/// Storage: `PezRewards::EpochRewardPools` (r:1 w:0)
|
||||
/// Proof: `PezRewards::EpochRewardPools` (`max_values`: None, `max_size`: Some(80), added: 2555, mode: `MaxEncodedLen`)
|
||||
/// Storage: `PezRewards::UserEpochScores` (r:1 w:0)
|
||||
/// Proof: `PezRewards::UserEpochScores` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Assets::Asset` (r:1 w:1)
|
||||
/// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Assets::Account` (r:2 w:2)
|
||||
/// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
|
||||
fn claim_reward() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `797`
|
||||
// Estimated: `6208`
|
||||
// Minimum execution time: 64_577_000 picoseconds.
|
||||
Weight::from_parts(67_351_000, 6208)
|
||||
.saturating_add(RocksDbWeight::get().reads(7_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(4_u64))
|
||||
}
|
||||
/// Storage: `PezRewards::EpochStatus` (r:1 w:1)
|
||||
/// Proof: `PezRewards::EpochStatus` (`max_values`: None, `max_size`: Some(21), added: 2496, mode: `MaxEncodedLen`)
|
||||
/// Storage: `PezRewards::EpochRewardPools` (r:1 w:0)
|
||||
/// Proof: `PezRewards::EpochRewardPools` (`max_values`: None, `max_size`: Some(80), added: 2555, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Assets::Account` (r:2 w:2)
|
||||
/// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Assets::Asset` (r:1 w:1)
|
||||
/// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
|
||||
/// Storage: `System::Account` (r:2 w:2)
|
||||
/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
|
||||
fn close_epoch() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `767`
|
||||
// Estimated: `6208`
|
||||
// Minimum execution time: 66_570_000 picoseconds.
|
||||
Weight::from_parts(70_662_000, 6208)
|
||||
.saturating_add(RocksDbWeight::get().reads(7_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(6_u64))
|
||||
}
|
||||
/// Storage: `PezRewards::ParliamentaryNftOwners` (r:0 w:1)
|
||||
/// Proof: `PezRewards::ParliamentaryNftOwners` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`)
|
||||
fn register_parliamentary_nft_owner() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `0`
|
||||
// Estimated: `0`
|
||||
// Minimum execution time: 7_785_000 picoseconds.
|
||||
Weight::from_parts(7_994_000, 0)
|
||||
.saturating_add(RocksDbWeight::get().writes(1_u64))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
[package]
|
||||
name = "pezpallet-pez-treasury"
|
||||
version = "1.0.0"
|
||||
description = "PezkuwiChain Programmatic Treasury with Synthetic Halving"
|
||||
authors.workspace = true
|
||||
homepage.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
publish = false
|
||||
repository.workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
||||
[dependencies]
|
||||
codec = { workspace = true, default-features = false, features = ["derive"] }
|
||||
scale-info = { default-features = false, features = [
|
||||
"derive",
|
||||
], workspace = true }
|
||||
serde = { version = "1.0", default-features = false, features = [
|
||||
"derive",
|
||||
], optional = true }
|
||||
|
||||
pezframe-support = { default-features = false, workspace = true }
|
||||
pezframe-system = { default-features = false, workspace = true }
|
||||
log = { default-features = false, workspace = true }
|
||||
pezsp-runtime = { default-features = false, workspace = true }
|
||||
pezsp-std = { default-features = false, workspace = true }
|
||||
|
||||
# PezkuwiChain'in özel tiplerini ve trait'lerini içeren kütüphane
|
||||
pezkuwi-primitives = { workspace = true, default-features = false }
|
||||
|
||||
# Standart Bizinikiwi paletleri
|
||||
pezpallet-balances = { default-features = false, workspace = true }
|
||||
pezpallet-scheduler = { default-features = false, workspace = true }
|
||||
|
||||
# Test ve Benchmark için Gerekli İsteğe Bağlı Bağımlılıklar
|
||||
pezframe-benchmarking = { optional = true, workspace = true }
|
||||
pezsp-core = { workspace = true, default-features = false, optional = true }
|
||||
pezsp-io = { workspace = true, default-features = false, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
# Test için gerekli olan bağımlılıklar
|
||||
pezsp-core = { workspace = true, default-features = false }
|
||||
pezsp-io = { workspace = true, default-features = false }
|
||||
pezpallet-assets = { workspace = true, default-features = false, features = [
|
||||
"std",
|
||||
] } # <-- BU SATIRI EKLEYİN
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = [
|
||||
"codec/std",
|
||||
"pezframe-benchmarking?/std",
|
||||
"pezframe-support/std",
|
||||
"pezframe-system/std",
|
||||
"log/std",
|
||||
"pezpallet-assets/std",
|
||||
"pezpallet-balances/std",
|
||||
"pezpallet-scheduler/std",
|
||||
"pezkuwi-primitives/std",
|
||||
"scale-info/std",
|
||||
"serde",
|
||||
"serde?/std",
|
||||
"pezsp-core?/std",
|
||||
"pezsp-io?/std",
|
||||
"pezsp-runtime/std",
|
||||
"pezsp-std/std",
|
||||
]
|
||||
runtime-benchmarks = [
|
||||
"pezframe-benchmarking",
|
||||
"pezframe-benchmarking/runtime-benchmarks",
|
||||
"pezframe-support/runtime-benchmarks",
|
||||
"pezframe-system/runtime-benchmarks",
|
||||
"pezpallet-assets/runtime-benchmarks",
|
||||
"pezpallet-balances/runtime-benchmarks",
|
||||
"pezpallet-scheduler/runtime-benchmarks",
|
||||
"pezkuwi-primitives/runtime-benchmarks",
|
||||
"pezsp-core",
|
||||
"pezsp-io",
|
||||
"pezsp-io?/runtime-benchmarks",
|
||||
"pezsp-runtime/runtime-benchmarks",
|
||||
]
|
||||
try-runtime = [
|
||||
"pezframe-support/try-runtime",
|
||||
"pezframe-system/try-runtime",
|
||||
"pezpallet-assets/try-runtime",
|
||||
"pezpallet-balances/try-runtime",
|
||||
"pezpallet-scheduler/try-runtime",
|
||||
"pezsp-runtime/try-runtime",
|
||||
]
|
||||
@@ -0,0 +1,92 @@
|
||||
// pezkuwi/pallets/pez-treasury/src/benchmarking.rs
|
||||
|
||||
#![cfg(feature = "runtime-benchmarks")]
|
||||
|
||||
use super::*;
|
||||
use crate::Pallet as PezTreasury;
|
||||
use pezframe_benchmarking::v2::*;
|
||||
use pezframe_support::traits::{
|
||||
fungibles::{Inspect, Mutate},
|
||||
Get, // HATA GİDERİLDİ: .get() fonksiyonu için bu trait eklendi
|
||||
};
|
||||
use pezframe_system::RawOrigin;
|
||||
use pezsp_runtime::traits::{Saturating, Zero};
|
||||
|
||||
#[benchmarks]
|
||||
mod benchmarks {
|
||||
use super::*;
|
||||
|
||||
#[benchmark]
|
||||
fn initialize_treasury() {
|
||||
crate::TreasuryStartBlock::<T>::kill();
|
||||
crate::HalvingInfo::<T>::kill();
|
||||
crate::NextReleaseMonth::<T>::kill();
|
||||
|
||||
#[extrinsic_call]
|
||||
initialize_treasury(RawOrigin::Root);
|
||||
|
||||
assert!(crate::TreasuryStartBlock::<T>::get().is_some());
|
||||
let halving_info = crate::HalvingInfo::<T>::get();
|
||||
assert_eq!(halving_info.current_period, 0);
|
||||
assert!(!halving_info.monthly_amount.is_zero());
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn force_genesis_distribution() {
|
||||
// Clear the flag to allow benchmark run (tests the new storage operation)
|
||||
crate::GenesisDistributionDone::<T>::kill();
|
||||
|
||||
#[block]
|
||||
{
|
||||
PezTreasury::<T>::do_genesis_distribution().unwrap();
|
||||
}
|
||||
|
||||
let treasury_account = PezTreasury::<T>::treasury_account_id();
|
||||
let presale_account = T::PresaleAccount::get();
|
||||
let founder_account = T::FounderAccount::get();
|
||||
|
||||
assert!(!T::Assets::balance(T::PezAssetId::get(), &treasury_account).is_zero());
|
||||
assert!(!T::Assets::balance(T::PezAssetId::get(), &presale_account).is_zero());
|
||||
assert!(!T::Assets::balance(T::PezAssetId::get(), &founder_account).is_zero());
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn release_monthly_funds() {
|
||||
// Setup
|
||||
crate::TreasuryStartBlock::<T>::kill();
|
||||
crate::HalvingInfo::<T>::kill();
|
||||
crate::NextReleaseMonth::<T>::kill();
|
||||
crate::GenesisDistributionDone::<T>::kill();
|
||||
// Deprecated `remove_all` yerine `clear` kullanılıyor.
|
||||
crate::MonthlyReleases::<T>::clear(u32::MAX, None);
|
||||
|
||||
// First do genesis distribution to properly fund the treasury
|
||||
PezTreasury::<T>::do_genesis_distribution().unwrap();
|
||||
PezTreasury::<T>::do_initialize_treasury().unwrap();
|
||||
|
||||
let treasury_account = PezTreasury::<T>::treasury_account_id();
|
||||
let initial_monthly_amount = PezTreasury::<T>::halving_info().monthly_amount;
|
||||
let incentive_amount = initial_monthly_amount * 75u32.into() / 100u32.into();
|
||||
let government_amount = initial_monthly_amount.saturating_sub(incentive_amount);
|
||||
|
||||
// Ensure treasury has MORE than enough balance for the release
|
||||
// Mint additional 10x the monthly amount to ensure sufficient balance
|
||||
let _ = T::Assets::mint_into(
|
||||
T::PezAssetId::get(),
|
||||
&treasury_account,
|
||||
initial_monthly_amount * 10u32.into(),
|
||||
);
|
||||
|
||||
let current_block = pezframe_system::Pallet::<T>::block_number();
|
||||
let target_block = current_block + crate::BLOCKS_PER_MONTH.into() + 1u32.into();
|
||||
pezframe_system::Pallet::<T>::set_block_number(target_block);
|
||||
|
||||
#[extrinsic_call]
|
||||
release_monthly_funds(RawOrigin::Root);
|
||||
|
||||
assert_eq!(PezTreasury::<T>::get_incentive_pot_balance(), incentive_amount);
|
||||
assert_eq!(PezTreasury::<T>::get_government_pot_balance(), government_amount);
|
||||
}
|
||||
|
||||
impl_benchmark_test_suite!(PezTreasury, crate::mock::new_test_ext(), crate::mock::Test);
|
||||
}
|
||||
@@ -0,0 +1,469 @@
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
//! # PEZ Treasury Pallet
|
||||
//!
|
||||
//! A pallet for managing the PEZ token distribution and treasury with automated halving mechanics.
|
||||
//!
|
||||
//! ## Overview
|
||||
//!
|
||||
//! This pallet manages the complete lifecycle of PEZ token distribution including:
|
||||
//!
|
||||
//! - **Genesis Distribution**: One-time initial distribution to treasury, presale, and founder
|
||||
//! accounts
|
||||
//! - **Halving Mechanism**: Automatic reduction of monthly releases every 48 months (4 years)
|
||||
//! - **Monthly Releases**: Scheduled distribution to incentive and government pots
|
||||
//! - **Multi-Pot System**: Separate accounts for treasury, incentive rewards, and governance
|
||||
//!
|
||||
//! ## Token Economics
|
||||
//!
|
||||
//! - **Total Supply**: 5,000,000,000 PEZ (5 billion tokens)
|
||||
//! - **Treasury Allocation**: 96.25% (4,812,500,000 PEZ)
|
||||
//! - **Presale Allocation**: 1.875% (93,750,000 PEZ)
|
||||
//! - **Founder Allocation**: 1.875% (93,750,000 PEZ)
|
||||
//!
|
||||
//! ## Halving Schedule
|
||||
//!
|
||||
//! - **Halving Period**: Every 48 months (4 years)
|
||||
//! - **Period Duration**: 20,736,000 blocks (~4 years at 10 blocks/minute)
|
||||
//! - **Distribution**: 70% to Incentive Pot, 30% to Government Pot
|
||||
//! - **Automatic Halving**: Monthly release amount halves at the start of each new period
|
||||
//!
|
||||
//! ## Security Features
|
||||
//!
|
||||
//! - **One-Time Genesis**: Genesis distribution can only occur once (protected by storage flag)
|
||||
//! - **Privileged Operations**: All extrinsics require privileged origin (root or governance)
|
||||
//! - **Block-Based Scheduling**: Monthly releases based on block numbers for determinism
|
||||
//!
|
||||
//! ## Interface
|
||||
//!
|
||||
//! ### Extrinsics
|
||||
//!
|
||||
//! - `force_genesis_distribution()` - Perform initial token distribution (one-time only,
|
||||
//! privileged)
|
||||
//! - `initialize_treasury()` - Initialize the halving mechanism and start monthly releases
|
||||
//! (privileged)
|
||||
//! - `release_monthly_funds()` - Release monthly funds to incentive and government pots
|
||||
//! (privileged)
|
||||
//!
|
||||
//! ### Storage
|
||||
//!
|
||||
//! - `HalvingInfo` - Current halving period data and monthly release amount
|
||||
//! - `MonthlyReleases` - Historical record of all monthly distributions
|
||||
//! - `GenesisDistributionDone` - Flag to prevent duplicate genesis distribution
|
||||
//!
|
||||
//! ### Runtime Integration Example
|
||||
//!
|
||||
//! ```ignore
|
||||
//! impl pezpallet_pez_treasury::Config for Runtime {
|
||||
//! type RuntimeEvent = RuntimeEvent;
|
||||
//! type Assets = Assets;
|
||||
//! type WeightInfo = pezpallet_pez_treasury::weights::BizinikiwiWeight<Runtime>;
|
||||
//! type PezAssetId = ConstU32<1>; // PEZ asset ID
|
||||
//! type TreasuryPalletId = TreasuryPalletId;
|
||||
//! type IncentivePotId = IncentivePotId;
|
||||
//! type GovernmentPotId = GovernmentPotId;
|
||||
//! type PresaleAccount = PresaleAccount;
|
||||
//! type FounderAccount = FounderAccount;
|
||||
//! type ForceOrigin = EnsureRoot<AccountId>;
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
pub use pallet::*;
|
||||
pub use weights::WeightInfo;
|
||||
|
||||
pub mod migrations;
|
||||
pub mod weights;
|
||||
|
||||
#[cfg(test)]
|
||||
mod mock;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
mod benchmarking;
|
||||
|
||||
use pezframe_support::{
|
||||
traits::{
|
||||
fungibles::{Inspect, Mutate},
|
||||
tokens::Preservation,
|
||||
Get,
|
||||
},
|
||||
PalletId,
|
||||
};
|
||||
use pezframe_system::pezpallet_prelude::BlockNumberFor;
|
||||
use scale_info::TypeInfo;
|
||||
use pezsp_runtime::traits::{AccountIdConversion, Saturating, Zero};
|
||||
|
||||
#[pezframe_support::pallet]
|
||||
pub mod pallet {
|
||||
use super::*;
|
||||
use pezframe_support::pezpallet_prelude::*;
|
||||
use pezframe_system::pezpallet_prelude::*;
|
||||
// use pezsp_runtime::traits::CheckedDiv;
|
||||
|
||||
pub const HALVING_PERIOD_MONTHS: u32 = 48; // 4 years = 48 months
|
||||
pub const BLOCKS_PER_MONTH: u32 = 432_000; // ~30 days * 24 hours * 60 minutes * 10 blocks/minute
|
||||
pub const HALVING_PERIOD_BLOCKS: u32 = HALVING_PERIOD_MONTHS * BLOCKS_PER_MONTH;
|
||||
|
||||
pub const TOTAL_SUPPLY: u128 = 5_000_000_000 * 1_000_000_000_000; // 5 billion PEZ (12 decimal)
|
||||
pub const TREASURY_ALLOCATION: u128 = 4_812_500_000 * 1_000_000_000_000; // %96.25
|
||||
pub const PRESALE_ALLOCATION: u128 = 93_750_000 * 1_000_000_000_000; // %1.875
|
||||
pub const FOUNDER_ALLOCATION: u128 = 93_750_000 * 1_000_000_000_000; // %1.875
|
||||
|
||||
#[pallet::pallet]
|
||||
#[pallet::storage_version(migrations::STORAGE_VERSION)]
|
||||
pub struct Pallet<T>(_);
|
||||
|
||||
#[pallet::config]
|
||||
pub trait Config: pezframe_system::Config + TypeInfo {
|
||||
type Assets: Mutate<Self::AccountId>;
|
||||
type WeightInfo: weights::WeightInfo;
|
||||
|
||||
#[pallet::constant]
|
||||
type PezAssetId: Get<<Self::Assets as Inspect<Self::AccountId>>::AssetId>;
|
||||
|
||||
#[pallet::constant]
|
||||
type TreasuryPalletId: Get<PalletId>;
|
||||
|
||||
#[pallet::constant]
|
||||
type IncentivePotId: Get<PalletId>;
|
||||
|
||||
#[pallet::constant]
|
||||
type GovernmentPotId: Get<PalletId>;
|
||||
|
||||
#[pallet::constant]
|
||||
type PresaleAccount: Get<Self::AccountId>;
|
||||
|
||||
#[pallet::constant]
|
||||
type FounderAccount: Get<Self::AccountId>;
|
||||
|
||||
type ForceOrigin: EnsureOrigin<Self::RuntimeOrigin>;
|
||||
}
|
||||
|
||||
pub type BalanceOf<T> =
|
||||
<<T as Config>::Assets as Inspect<<T as pezframe_system::Config>::AccountId>>::Balance;
|
||||
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn halving_info)]
|
||||
pub type HalvingInfo<T: Config> = StorageValue<_, HalvingData<T>, ValueQuery>;
|
||||
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn monthly_releases)]
|
||||
pub type MonthlyReleases<T: Config> =
|
||||
StorageMap<_, Blake2_128Concat, u32, MonthlyRelease<T>, OptionQuery>;
|
||||
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn next_release_month)]
|
||||
pub type NextReleaseMonth<T: Config> = StorageValue<_, u32, ValueQuery>;
|
||||
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn treasury_start_block)]
|
||||
pub type TreasuryStartBlock<T: Config> = StorageValue<_, BlockNumberFor<T>, OptionQuery>;
|
||||
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn genesis_distribution_done)]
|
||||
pub type GenesisDistributionDone<T: Config> = StorageValue<_, bool, ValueQuery>;
|
||||
|
||||
#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
|
||||
#[scale_info(skip_type_params(T))]
|
||||
pub struct HalvingData<T: Config> {
|
||||
pub current_period: u32,
|
||||
pub period_start_block: BlockNumberFor<T>,
|
||||
pub monthly_amount: BalanceOf<T>,
|
||||
pub total_released: BalanceOf<T>,
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
|
||||
#[scale_info(skip_type_params(T))]
|
||||
pub struct MonthlyRelease<T: Config> {
|
||||
pub month_index: u32,
|
||||
pub release_block: BlockNumberFor<T>,
|
||||
pub amount_released: BalanceOf<T>,
|
||||
pub incentive_amount: BalanceOf<T>,
|
||||
pub government_amount: BalanceOf<T>,
|
||||
}
|
||||
|
||||
impl<T: Config> Default for HalvingData<T> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
current_period: 0,
|
||||
period_start_block: Zero::zero(),
|
||||
monthly_amount: Zero::zero(),
|
||||
total_released: Zero::zero(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[pallet::event]
|
||||
#[pallet::generate_deposit(pub(super) fn deposit_event)]
|
||||
pub enum Event<T: Config> {
|
||||
TreasuryInitialized {
|
||||
start_block: BlockNumberFor<T>,
|
||||
initial_monthly_amount: BalanceOf<T>,
|
||||
},
|
||||
MonthlyFundsReleased {
|
||||
month_index: u32,
|
||||
total_amount: BalanceOf<T>,
|
||||
incentive_amount: BalanceOf<T>,
|
||||
government_amount: BalanceOf<T>,
|
||||
},
|
||||
NewHalvingPeriod {
|
||||
period: u32,
|
||||
new_monthly_amount: BalanceOf<T>,
|
||||
},
|
||||
GenesisDistributionCompleted {
|
||||
treasury_amount: BalanceOf<T>,
|
||||
presale_amount: BalanceOf<T>,
|
||||
founder_amount: BalanceOf<T>,
|
||||
},
|
||||
}
|
||||
|
||||
#[pallet::error]
|
||||
pub enum Error<T> {
|
||||
TreasuryAlreadyInitialized,
|
||||
TreasuryNotInitialized,
|
||||
MonthlyReleaseAlreadyDone,
|
||||
InsufficientTreasuryBalance,
|
||||
InvalidHalvingPeriod,
|
||||
ReleaseTooEarly,
|
||||
GenesisDistributionAlreadyDone,
|
||||
}
|
||||
|
||||
#[pallet::genesis_config]
|
||||
#[derive(pezframe_support::DefaultNoBound)]
|
||||
pub struct GenesisConfig<T: Config> {
|
||||
pub initialize_treasury: bool,
|
||||
#[serde(skip)]
|
||||
pub _phantom: core::marker::PhantomData<T>,
|
||||
}
|
||||
|
||||
#[pallet::genesis_build]
|
||||
impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
|
||||
fn build(&self) {
|
||||
if self.initialize_treasury {
|
||||
let _ = Pallet::<T>::do_initialize_treasury();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[pallet::call]
|
||||
impl<T: Config> Pallet<T> {
|
||||
#[pallet::call_index(0)]
|
||||
#[pallet::weight(T::WeightInfo::initialize_treasury())]
|
||||
pub fn initialize_treasury(origin: OriginFor<T>) -> DispatchResult {
|
||||
T::ForceOrigin::ensure_origin(origin)?;
|
||||
Self::do_initialize_treasury()
|
||||
}
|
||||
|
||||
#[pallet::call_index(1)]
|
||||
#[pallet::weight(T::WeightInfo::release_monthly_funds())]
|
||||
pub fn release_monthly_funds(origin: OriginFor<T>) -> DispatchResult {
|
||||
T::ForceOrigin::ensure_origin(origin)?;
|
||||
Self::do_monthly_release()
|
||||
}
|
||||
|
||||
#[pallet::call_index(2)]
|
||||
#[pallet::weight(T::WeightInfo::force_genesis_distribution())]
|
||||
pub fn force_genesis_distribution(origin: OriginFor<T>) -> DispatchResult {
|
||||
T::ForceOrigin::ensure_origin(origin)?;
|
||||
Self::do_genesis_distribution()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> Pallet<T> {
|
||||
pub fn treasury_account_id() -> T::AccountId {
|
||||
T::TreasuryPalletId::get().into_account_truncating()
|
||||
}
|
||||
|
||||
pub fn incentive_pot_account_id() -> T::AccountId {
|
||||
T::IncentivePotId::get().into_account_truncating()
|
||||
}
|
||||
|
||||
pub fn government_pot_account_id() -> T::AccountId {
|
||||
T::GovernmentPotId::get().into_account_truncating()
|
||||
}
|
||||
|
||||
pub fn do_genesis_distribution() -> DispatchResult {
|
||||
// SECURITY: Ensure genesis distribution can only happen once
|
||||
ensure!(
|
||||
!GenesisDistributionDone::<T>::get(),
|
||||
Error::<T>::GenesisDistributionAlreadyDone
|
||||
);
|
||||
|
||||
let treasury_account = Self::treasury_account_id();
|
||||
let presale_account = T::PresaleAccount::get();
|
||||
let founder_account = T::FounderAccount::get();
|
||||
|
||||
let treasury_amount: BalanceOf<T> = TREASURY_ALLOCATION
|
||||
.try_into()
|
||||
.map_err(|_| Error::<T>::InsufficientTreasuryBalance)?;
|
||||
let presale_amount: BalanceOf<T> = PRESALE_ALLOCATION
|
||||
.try_into()
|
||||
.map_err(|_| Error::<T>::InsufficientTreasuryBalance)?;
|
||||
let founder_amount: BalanceOf<T> = FOUNDER_ALLOCATION
|
||||
.try_into()
|
||||
.map_err(|_| Error::<T>::InsufficientTreasuryBalance)?;
|
||||
|
||||
T::Assets::mint_into(T::PezAssetId::get(), &treasury_account, treasury_amount)?;
|
||||
T::Assets::mint_into(T::PezAssetId::get(), &presale_account, presale_amount)?;
|
||||
T::Assets::mint_into(T::PezAssetId::get(), &founder_account, founder_amount)?;
|
||||
|
||||
// Mark genesis distribution as completed
|
||||
GenesisDistributionDone::<T>::put(true);
|
||||
|
||||
Self::deposit_event(Event::GenesisDistributionCompleted {
|
||||
treasury_amount,
|
||||
presale_amount,
|
||||
founder_amount,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn do_initialize_treasury() -> DispatchResult {
|
||||
ensure!(
|
||||
TreasuryStartBlock::<T>::get().is_none(),
|
||||
Error::<T>::TreasuryAlreadyInitialized
|
||||
);
|
||||
|
||||
let current_block = pezframe_system::Pallet::<T>::block_number();
|
||||
|
||||
let treasury_balance = TREASURY_ALLOCATION;
|
||||
let first_period_total =
|
||||
treasury_balance.checked_div(2).ok_or(Error::<T>::InvalidHalvingPeriod)?;
|
||||
let monthly_amount = first_period_total
|
||||
.checked_div(HALVING_PERIOD_MONTHS.into())
|
||||
.ok_or(Error::<T>::InvalidHalvingPeriod)?;
|
||||
|
||||
let monthly_amount_balance: BalanceOf<T> =
|
||||
monthly_amount.try_into().map_err(|_| Error::<T>::InsufficientTreasuryBalance)?;
|
||||
|
||||
let halving_data = HalvingData {
|
||||
current_period: 0,
|
||||
period_start_block: current_block,
|
||||
monthly_amount: monthly_amount_balance,
|
||||
total_released: Zero::zero(),
|
||||
};
|
||||
|
||||
TreasuryStartBlock::<T>::put(current_block);
|
||||
HalvingInfo::<T>::put(halving_data);
|
||||
NextReleaseMonth::<T>::put(0);
|
||||
|
||||
Self::deposit_event(Event::TreasuryInitialized {
|
||||
start_block: current_block,
|
||||
initial_monthly_amount: monthly_amount_balance,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn do_monthly_release() -> DispatchResult {
|
||||
ensure!(TreasuryStartBlock::<T>::get().is_some(), Error::<T>::TreasuryNotInitialized);
|
||||
|
||||
let current_block = pezframe_system::Pallet::<T>::block_number();
|
||||
let start_block = TreasuryStartBlock::<T>::get().unwrap();
|
||||
let next_month = NextReleaseMonth::<T>::get();
|
||||
|
||||
ensure!(
|
||||
!MonthlyReleases::<T>::contains_key(next_month),
|
||||
Error::<T>::MonthlyReleaseAlreadyDone
|
||||
);
|
||||
|
||||
let blocks_passed = current_block.saturating_sub(start_block);
|
||||
let months_passed: u32 = (blocks_passed / BLOCKS_PER_MONTH.into())
|
||||
.try_into()
|
||||
.map_err(|_| Error::<T>::InvalidHalvingPeriod)?;
|
||||
|
||||
// To release month 0, months_passed must be >= 1 (next_month + 1)
|
||||
// To release month 1, months_passed must be >= 2
|
||||
ensure!(months_passed > next_month, Error::<T>::ReleaseTooEarly);
|
||||
|
||||
let mut halving_data = HalvingInfo::<T>::get();
|
||||
|
||||
let current_period_passed_months =
|
||||
months_passed.saturating_sub(halving_data.current_period * HALVING_PERIOD_MONTHS);
|
||||
|
||||
if current_period_passed_months >= HALVING_PERIOD_MONTHS {
|
||||
halving_data.current_period = halving_data.current_period.saturating_add(1);
|
||||
halving_data.monthly_amount = halving_data
|
||||
.monthly_amount
|
||||
.checked_div(&2u32.into())
|
||||
.ok_or(Error::<T>::InvalidHalvingPeriod)?;
|
||||
halving_data.period_start_block = current_block;
|
||||
|
||||
Self::deposit_event(Event::NewHalvingPeriod {
|
||||
period: halving_data.current_period,
|
||||
new_monthly_amount: halving_data.monthly_amount,
|
||||
});
|
||||
}
|
||||
|
||||
let monthly_amount = halving_data.monthly_amount;
|
||||
let incentive_amount = monthly_amount
|
||||
.checked_mul(&75u32.into())
|
||||
.and_then(|v| v.checked_div(&100u32.into()))
|
||||
.ok_or(Error::<T>::InvalidHalvingPeriod)?;
|
||||
let government_amount = monthly_amount.saturating_sub(incentive_amount);
|
||||
|
||||
let treasury_account = Self::treasury_account_id();
|
||||
let incentive_pot = Self::incentive_pot_account_id();
|
||||
let government_pot = Self::government_pot_account_id();
|
||||
|
||||
T::Assets::transfer(
|
||||
T::PezAssetId::get(),
|
||||
&treasury_account,
|
||||
&incentive_pot,
|
||||
incentive_amount,
|
||||
Preservation::Preserve,
|
||||
)
|
||||
.map_err(|_| Error::<T>::InsufficientTreasuryBalance)?;
|
||||
|
||||
T::Assets::transfer(
|
||||
T::PezAssetId::get(),
|
||||
&treasury_account,
|
||||
&government_pot,
|
||||
government_amount,
|
||||
Preservation::Preserve,
|
||||
)
|
||||
.map_err(|_| Error::<T>::InsufficientTreasuryBalance)?;
|
||||
|
||||
halving_data.total_released =
|
||||
halving_data.total_released.saturating_add(monthly_amount);
|
||||
HalvingInfo::<T>::put(halving_data);
|
||||
|
||||
let release_info = MonthlyRelease {
|
||||
month_index: next_month,
|
||||
release_block: current_block,
|
||||
amount_released: monthly_amount,
|
||||
incentive_amount,
|
||||
government_amount,
|
||||
};
|
||||
|
||||
MonthlyReleases::<T>::insert(next_month, release_info);
|
||||
NextReleaseMonth::<T>::put(next_month + 1);
|
||||
|
||||
Self::deposit_event(Event::MonthlyFundsReleased {
|
||||
month_index: next_month,
|
||||
total_amount: monthly_amount,
|
||||
incentive_amount,
|
||||
government_amount,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_current_halving_info() -> HalvingData<T> {
|
||||
HalvingInfo::<T>::get()
|
||||
}
|
||||
|
||||
pub fn get_incentive_pot_balance() -> BalanceOf<T> {
|
||||
let pot_account = Self::incentive_pot_account_id();
|
||||
T::Assets::balance(T::PezAssetId::get(), &pot_account)
|
||||
}
|
||||
|
||||
pub fn get_government_pot_balance() -> BalanceOf<T> {
|
||||
let pot_account = Self::government_pot_account_id();
|
||||
T::Assets::balance(T::PezAssetId::get(), &pot_account)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
//! Storage migrations for pezpallet-pez-treasury
|
||||
|
||||
use super::*;
|
||||
use pezframe_support::{
|
||||
traits::{Get, GetStorageVersion, OnRuntimeUpgrade, StorageVersion},
|
||||
weights::Weight,
|
||||
};
|
||||
use pezsp_std::marker::PhantomData;
|
||||
|
||||
/// Current storage version
|
||||
pub const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
|
||||
|
||||
/// Migration from v0 to v1
|
||||
/// This migration handles the initial version setup for pezpallet-pez-treasury
|
||||
pub mod v1 {
|
||||
use super::*;
|
||||
|
||||
pub struct MigrateToV1<T>(PhantomData<T>);
|
||||
|
||||
impl<T: Config> OnRuntimeUpgrade for MigrateToV1<T> {
|
||||
fn on_runtime_upgrade() -> Weight {
|
||||
let current = Pallet::<T>::on_chain_storage_version();
|
||||
|
||||
log::info!(
|
||||
"🔄 Running migration for pezpallet-pez-treasury from {:?} to {:?}",
|
||||
current,
|
||||
STORAGE_VERSION
|
||||
);
|
||||
|
||||
if current == StorageVersion::new(0) {
|
||||
let mut weight = Weight::zero();
|
||||
|
||||
// Example migration logic for treasury storage
|
||||
// If storage format changes in the future, implement transformation here
|
||||
|
||||
// Count existing storage items for logging
|
||||
let monthly_releases_count = MonthlyReleases::<T>::iter().count() as u64;
|
||||
let has_halving_info = if HalvingInfo::<T>::exists() { 1u64 } else { 0u64 };
|
||||
let has_treasury_start =
|
||||
if TreasuryStartBlock::<T>::get().is_some() { 1u64 } else { 0u64 };
|
||||
let has_genesis_done =
|
||||
if GenesisDistributionDone::<T>::get() { 1u64 } else { 0u64 };
|
||||
|
||||
let migrated = monthly_releases_count +
|
||||
has_halving_info +
|
||||
has_treasury_start +
|
||||
has_genesis_done;
|
||||
|
||||
// Update storage version
|
||||
STORAGE_VERSION.put::<Pallet<T>>();
|
||||
|
||||
log::info!("✅ Migrated {} entries in pezpallet-pez-treasury", migrated);
|
||||
log::info!(" MonthlyReleases: {}, HalvingInfo: {}, TreasuryStartBlock: {}, GenesisDistributionDone: {}",
|
||||
monthly_releases_count, has_halving_info, has_treasury_start, has_genesis_done);
|
||||
|
||||
// Return weight used
|
||||
// Reads: all storage items + version read
|
||||
// Writes: version write
|
||||
weight = weight.saturating_add(T::DbWeight::get().reads_writes(migrated + 1, 1));
|
||||
|
||||
weight
|
||||
} else {
|
||||
log::info!(
|
||||
"👌 pezpallet-pez-treasury migration not needed, current version is {:?}",
|
||||
current
|
||||
);
|
||||
T::DbWeight::get().reads(1)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn pre_upgrade() -> Result<pezsp_std::vec::Vec<u8>, pezsp_runtime::TryRuntimeError> {
|
||||
let current = Pallet::<T>::on_chain_storage_version();
|
||||
|
||||
log::info!("🔍 Pre-upgrade check for pezpallet-pez-treasury");
|
||||
log::info!(" Current version: {:?}", current);
|
||||
|
||||
// Encode current storage counts for verification
|
||||
let monthly_releases_count = MonthlyReleases::<T>::iter().count() as u32;
|
||||
let next_release_month = NextReleaseMonth::<T>::get();
|
||||
let has_treasury_start = TreasuryStartBlock::<T>::get().is_some();
|
||||
let genesis_done = GenesisDistributionDone::<T>::get();
|
||||
|
||||
log::info!(" MonthlyReleases entries: {}", monthly_releases_count);
|
||||
log::info!(" NextReleaseMonth: {}", next_release_month);
|
||||
log::info!(" TreasuryStartBlock exists: {}", has_treasury_start);
|
||||
log::info!(" GenesisDistributionDone: {}", genesis_done);
|
||||
|
||||
Ok((monthly_releases_count, next_release_month, has_treasury_start, genesis_done)
|
||||
.encode())
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn post_upgrade(state: pezsp_std::vec::Vec<u8>) -> Result<(), pezsp_runtime::TryRuntimeError> {
|
||||
use codec::Decode;
|
||||
|
||||
let (
|
||||
pre_monthly_releases_count,
|
||||
pre_next_release_month,
|
||||
pre_has_treasury_start,
|
||||
pre_genesis_done,
|
||||
): (u32, u32, bool, bool) = Decode::decode(&mut &state[..])
|
||||
.map_err(|_| "Failed to decode pre-upgrade state")?;
|
||||
|
||||
log::info!("🔍 Post-upgrade check for pezpallet-pez-treasury");
|
||||
|
||||
// Verify storage version was updated
|
||||
let current_version = Pallet::<T>::on_chain_storage_version();
|
||||
assert_eq!(current_version, STORAGE_VERSION, "Storage version not updated correctly");
|
||||
log::info!("✅ Storage version updated to {:?}", current_version);
|
||||
|
||||
// Verify storage counts (should be same or more, never less)
|
||||
let post_monthly_releases_count = MonthlyReleases::<T>::iter().count() as u32;
|
||||
let post_next_release_month = NextReleaseMonth::<T>::get();
|
||||
let post_has_treasury_start = TreasuryStartBlock::<T>::get().is_some();
|
||||
let post_genesis_done = GenesisDistributionDone::<T>::get();
|
||||
|
||||
log::info!(
|
||||
" MonthlyReleases entries: {} -> {}",
|
||||
pre_monthly_releases_count,
|
||||
post_monthly_releases_count
|
||||
);
|
||||
log::info!(
|
||||
" NextReleaseMonth: {} -> {}",
|
||||
pre_next_release_month,
|
||||
post_next_release_month
|
||||
);
|
||||
log::info!(
|
||||
" TreasuryStartBlock exists: {} -> {}",
|
||||
pre_has_treasury_start,
|
||||
post_has_treasury_start
|
||||
);
|
||||
log::info!(" GenesisDistributionDone: {} -> {}", pre_genesis_done, post_genesis_done);
|
||||
|
||||
// Verify no data was lost
|
||||
assert!(
|
||||
post_monthly_releases_count >= pre_monthly_releases_count,
|
||||
"MonthlyReleases entries decreased during migration"
|
||||
);
|
||||
|
||||
// NextReleaseMonth should not decrease
|
||||
assert!(
|
||||
post_next_release_month >= pre_next_release_month,
|
||||
"NextReleaseMonth decreased during migration"
|
||||
);
|
||||
|
||||
// Treasury start block should not be removed if it existed
|
||||
if pre_has_treasury_start {
|
||||
assert!(post_has_treasury_start, "TreasuryStartBlock was removed during migration");
|
||||
}
|
||||
|
||||
// Genesis done flag should not change from true to false
|
||||
if pre_genesis_done {
|
||||
assert!(post_genesis_done, "GenesisDistributionDone was reset during migration");
|
||||
}
|
||||
|
||||
log::info!("✅ Post-upgrade checks passed for pezpallet-pez-treasury");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Example migration for future version changes
|
||||
/// This demonstrates how to handle storage format changes in treasury data
|
||||
pub mod v2 {
|
||||
use super::*;
|
||||
|
||||
/// Example: Migration when halving data or release format changes
|
||||
pub struct MigrateToV2<T>(PhantomData<T>);
|
||||
|
||||
impl<T: Config> OnRuntimeUpgrade for MigrateToV2<T> {
|
||||
fn on_runtime_upgrade() -> Weight {
|
||||
let current = Pallet::<T>::on_chain_storage_version();
|
||||
|
||||
if current < StorageVersion::new(2) {
|
||||
log::info!("🔄 Running migration for pezpallet-pez-treasury to v2");
|
||||
|
||||
// Example migration logic
|
||||
// 1. Transform halving data if format changed
|
||||
// 2. Migrate monthly release records if needed
|
||||
// 3. Update version
|
||||
|
||||
// For now, this is just a template
|
||||
StorageVersion::new(2).put::<Pallet<T>>();
|
||||
|
||||
log::info!("✅ Completed migration to pezpallet-pez-treasury v2");
|
||||
|
||||
T::DbWeight::get().reads_writes(1, 1)
|
||||
} else {
|
||||
log::info!("👌 pezpallet-pez-treasury v2 migration not needed");
|
||||
T::DbWeight::get().reads(1)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn pre_upgrade() -> Result<pezsp_std::vec::Vec<u8>, pezsp_runtime::TryRuntimeError> {
|
||||
log::info!("🔍 Pre-upgrade check for pezpallet-pez-treasury v2");
|
||||
Ok(pezsp_std::vec::Vec::new())
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn post_upgrade(_state: pezsp_std::vec::Vec<u8>) -> Result<(), pezsp_runtime::TryRuntimeError> {
|
||||
log::info!("✅ Post-upgrade check passed for pezpallet-pez-treasury v2");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::mock::{new_test_ext, Test};
|
||||
use pezframe_support::traits::OnRuntimeUpgrade;
|
||||
|
||||
#[test]
|
||||
fn test_migration_v1() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// Set initial storage version to 0
|
||||
StorageVersion::new(0).put::<Pallet<Test>>();
|
||||
|
||||
// Run migration
|
||||
let weight = v1::MigrateToV1::<Test>::on_runtime_upgrade();
|
||||
|
||||
// Verify version was updated
|
||||
assert_eq!(Pallet::<Test>::on_chain_storage_version(), STORAGE_VERSION);
|
||||
|
||||
// Verify weight is non-zero
|
||||
assert!(weight != Weight::zero());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_migration_idempotent() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// Set current version
|
||||
STORAGE_VERSION.put::<Pallet<Test>>();
|
||||
|
||||
// Run migration again
|
||||
let weight = v1::MigrateToV1::<Test>::on_runtime_upgrade();
|
||||
|
||||
// Should be a no-op
|
||||
assert_eq!(weight, pezframe_support::weights::constants::RocksDbWeight::get().reads(1));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
// pezkuwi/pallets/pez-treasury/src/mock.rs
|
||||
// VERSION 3: AccountId tipi H256 yapıldı (u64 yerine)
|
||||
|
||||
use crate as pezpallet_pez_treasury;
|
||||
use pezframe_support::{
|
||||
assert_ok, construct_runtime, parameter_types,
|
||||
traits::{ConstU128, ConstU32, OnFinalize, OnInitialize},
|
||||
PalletId,
|
||||
};
|
||||
use pezsp_core::H256;
|
||||
use pezsp_runtime::{
|
||||
traits::{BlakeTwo256, IdentityLookup},
|
||||
BuildStorage,
|
||||
};
|
||||
|
||||
type Block = pezframe_system::mocking::MockBlock<Test>;
|
||||
|
||||
construct_runtime!(
|
||||
pub enum Test
|
||||
{
|
||||
System: pezframe_system,
|
||||
Balances: pezpallet_balances,
|
||||
Assets: pezpallet_assets,
|
||||
PezTreasury: pezpallet_pez_treasury,
|
||||
}
|
||||
);
|
||||
|
||||
parameter_types! {
|
||||
pub const BlockHashCount: u64 = 250;
|
||||
}
|
||||
|
||||
impl pezframe_system::Config for Test {
|
||||
type BaseCallFilter = pezframe_support::traits::Everything;
|
||||
type BlockWeights = ();
|
||||
type BlockLength = ();
|
||||
type DbWeight = pezframe_support::weights::constants::RocksDbWeight;
|
||||
type RuntimeOrigin = RuntimeOrigin;
|
||||
type RuntimeCall = RuntimeCall;
|
||||
type Nonce = u64;
|
||||
type Hash = H256;
|
||||
type Hashing = BlakeTwo256;
|
||||
type AccountId = H256; // V3: u64 -> H256 değişti
|
||||
type Lookup = IdentityLookup<Self::AccountId>;
|
||||
type Block = Block;
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type BlockHashCount = BlockHashCount;
|
||||
type Version = ();
|
||||
type PalletInfo = PalletInfo;
|
||||
type AccountData = pezpallet_balances::AccountData<u128>;
|
||||
type OnNewAccount = ();
|
||||
type OnKilledAccount = ();
|
||||
type SystemWeightInfo = ();
|
||||
type SS58Prefix = ();
|
||||
type OnSetCode = ();
|
||||
type MaxConsumers = ConstU32<16>;
|
||||
type SingleBlockMigrations = ();
|
||||
type MultiBlockMigrator = ();
|
||||
type PreInherents = ();
|
||||
type PostInherents = ();
|
||||
type PostTransactions = ();
|
||||
type RuntimeTask = ();
|
||||
type ExtensionsWeightInfo = ();
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const ExistentialDeposit: u128 = 1;
|
||||
}
|
||||
|
||||
impl pezpallet_balances::Config for Test {
|
||||
type MaxLocks = ();
|
||||
type MaxReserves = ();
|
||||
type ReserveIdentifier = [u8; 8];
|
||||
type Balance = u128;
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type DustRemoval = ();
|
||||
type ExistentialDeposit = ExistentialDeposit;
|
||||
type AccountStore = System;
|
||||
type WeightInfo = ();
|
||||
type FreezeIdentifier = ();
|
||||
type MaxFreezes = ();
|
||||
type RuntimeHoldReason = ();
|
||||
type RuntimeFreezeReason = ();
|
||||
type DoneSlashHandler = ();
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const AssetDeposit: u128 = 100;
|
||||
pub const ApprovalDeposit: u128 = 1;
|
||||
pub const StringLimit: u32 = 50;
|
||||
pub const MetadataDepositBase: u128 = 10;
|
||||
pub const MetadataDepositPerByte: u128 = 1;
|
||||
}
|
||||
|
||||
impl pezpallet_assets::Config for Test {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type Balance = u128;
|
||||
type AssetId = u32;
|
||||
type AssetIdParameter = u32;
|
||||
type Currency = Balances;
|
||||
type CreateOrigin =
|
||||
pezframe_support::traits::AsEnsureOriginWithArg<pezframe_system::EnsureSigned<Self::AccountId>>;
|
||||
type ForceOrigin = pezframe_system::EnsureRoot<Self::AccountId>;
|
||||
type AssetDeposit = AssetDeposit;
|
||||
type AssetAccountDeposit = ConstU128<0>;
|
||||
type MetadataDepositBase = MetadataDepositBase;
|
||||
type MetadataDepositPerByte = MetadataDepositPerByte;
|
||||
type ApprovalDeposit = ApprovalDeposit;
|
||||
type StringLimit = StringLimit;
|
||||
type Freezer = ();
|
||||
type Extra = ();
|
||||
type CallbackHandle = ();
|
||||
type WeightInfo = ();
|
||||
type RemoveItemsLimit = ConstU32<1000>;
|
||||
type Holder = ();
|
||||
type ReserveData = ();
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
type BenchmarkHelper = ();
|
||||
}
|
||||
|
||||
// CRITICAL: Bu üç PalletId FARKLI olmak ZORUNDA
|
||||
parameter_types! {
|
||||
pub const PezTreasuryPalletId: PalletId = PalletId(*b"py/pztrs");
|
||||
pub const PezIncentivePotId: PalletId = PalletId(*b"py/pzinc");
|
||||
pub const PezGovernmentPotId: PalletId = PalletId(*b"py/pzgov");
|
||||
pub const PezAssetId: u32 = 1;
|
||||
}
|
||||
|
||||
// V3: Test accounts - H256 formatında
|
||||
use pezsp_runtime::traits::AccountIdConversion;
|
||||
|
||||
pub fn alice() -> H256 {
|
||||
H256::from_low_u64_be(1)
|
||||
}
|
||||
|
||||
pub fn bob() -> H256 {
|
||||
H256::from_low_u64_be(2)
|
||||
}
|
||||
|
||||
pub fn charlie() -> H256 {
|
||||
H256::from_low_u64_be(3)
|
||||
}
|
||||
|
||||
pub fn presale() -> H256 {
|
||||
H256::from_low_u64_be(10)
|
||||
}
|
||||
|
||||
pub fn founder() -> H256 {
|
||||
H256::from_low_u64_be(11)
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub PresaleAccount: H256 = presale();
|
||||
pub FounderAccount: H256 = founder();
|
||||
}
|
||||
|
||||
impl pezpallet_pez_treasury::Config for Test {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type Assets = Assets;
|
||||
type WeightInfo = ();
|
||||
type PezAssetId = PezAssetId;
|
||||
type TreasuryPalletId = PezTreasuryPalletId;
|
||||
type IncentivePotId = PezIncentivePotId;
|
||||
type GovernmentPotId = PezGovernmentPotId;
|
||||
type PresaleAccount = PresaleAccount;
|
||||
type FounderAccount = FounderAccount;
|
||||
type ForceOrigin = pezframe_system::EnsureRoot<Self::AccountId>;
|
||||
}
|
||||
|
||||
// Build genesis storage according to the mock runtime.
|
||||
pub fn new_test_ext() -> pezsp_io::TestExternalities {
|
||||
let mut t = pezframe_system::GenesisConfig::<Test>::default().build_storage().unwrap();
|
||||
|
||||
pezpallet_balances::GenesisConfig::<Test> {
|
||||
balances: vec![
|
||||
(alice(), 1_000_000_000_000_000),
|
||||
(bob(), 1_000_000_000_000_000),
|
||||
(charlie(), 1_000_000_000_000_000),
|
||||
(presale(), 1_000_000_000_000_000),
|
||||
(founder(), 1_000_000_000_000_000),
|
||||
],
|
||||
dev_accounts: None,
|
||||
}
|
||||
.assimilate_storage(&mut t)
|
||||
.unwrap();
|
||||
|
||||
let mut ext = pezsp_io::TestExternalities::new(t);
|
||||
ext.execute_with(|| {
|
||||
System::set_block_number(1);
|
||||
|
||||
// Create PEZ asset
|
||||
assert_ok!(Assets::force_create(
|
||||
RuntimeOrigin::root(),
|
||||
PezAssetId::get(),
|
||||
alice(),
|
||||
true,
|
||||
1
|
||||
));
|
||||
});
|
||||
ext
|
||||
}
|
||||
|
||||
// Helper function to run to specific block
|
||||
pub fn run_to_block(n: u64) {
|
||||
while System::block_number() < n {
|
||||
if System::block_number() > 1 {
|
||||
AllPalletsWithSystem::on_finalize(System::block_number());
|
||||
}
|
||||
System::set_block_number(System::block_number() + 1);
|
||||
AllPalletsWithSystem::on_initialize(System::block_number());
|
||||
}
|
||||
}
|
||||
|
||||
// V3: Helper to assert balance - H256 account ile
|
||||
pub fn assert_pez_balance(account: H256, expected: u128) {
|
||||
assert_eq!(
|
||||
Assets::balance(PezAssetId::get(), account),
|
||||
expected,
|
||||
"PEZ balance mismatch for account {:?}. Expected: {}, Got: {}",
|
||||
account,
|
||||
expected,
|
||||
Assets::balance(PezAssetId::get(), account)
|
||||
);
|
||||
}
|
||||
|
||||
// V3: Helper fonksiyonlar - H256 dönüyor
|
||||
#[allow(dead_code)]
|
||||
pub fn treasury_account() -> H256 {
|
||||
PezTreasuryPalletId::get().into_account_truncating()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn incentive_pot_account() -> H256 {
|
||||
PezIncentivePotId::get().into_account_truncating()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn government_pot_account() -> H256 {
|
||||
PezGovernmentPotId::get().into_account_truncating()
|
||||
}
|
||||
|
||||
// V3: Debug helper
|
||||
#[allow(dead_code)]
|
||||
pub fn debug_pot_accounts() {
|
||||
println!("\n=== PalletId Debug ===");
|
||||
println!("Treasury bytes: {:?}", PezTreasuryPalletId::get().0);
|
||||
println!("Incentive bytes: {:?}", PezIncentivePotId::get().0);
|
||||
println!("Government bytes: {:?}", PezGovernmentPotId::get().0);
|
||||
println!("======================\n");
|
||||
}
|
||||
@@ -0,0 +1,969 @@
|
||||
// pezkuwi/pallets/pez-treasury/src/tests.rs
|
||||
|
||||
use crate::{mock::*, Error, Event};
|
||||
use pezframe_support::{assert_noop, assert_ok};
|
||||
use pezsp_runtime::traits::Zero; // FIXED: Import Zero trait for is_zero() method
|
||||
|
||||
// =============================================================================
|
||||
// 1. GENESIS DISTRIBUTION TESTS
|
||||
// =============================================================================
|
||||
|
||||
#[test]
|
||||
fn genesis_distribution_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(PezTreasury::do_genesis_distribution());
|
||||
|
||||
let treasury_amount = 4_812_500_000 * 1_000_000_000_000u128;
|
||||
let presale_amount = 93_750_000 * 1_000_000_000_000u128;
|
||||
let founder_amount = 93_750_000 * 1_000_000_000_000u128;
|
||||
|
||||
assert_pez_balance(treasury_account(), treasury_amount);
|
||||
assert_pez_balance(presale(), presale_amount);
|
||||
assert_pez_balance(founder(), founder_amount);
|
||||
|
||||
let total = treasury_amount + presale_amount + founder_amount;
|
||||
assert_eq!(total, 5_000_000_000 * 1_000_000_000_000u128);
|
||||
|
||||
System::assert_has_event(
|
||||
Event::GenesisDistributionCompleted { treasury_amount, presale_amount, founder_amount }
|
||||
.into(),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn force_genesis_distribution_requires_root() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_noop!(
|
||||
PezTreasury::force_genesis_distribution(RuntimeOrigin::signed(alice())),
|
||||
pezsp_runtime::DispatchError::BadOrigin
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn force_genesis_distribution_works_with_root() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(PezTreasury::force_genesis_distribution(RuntimeOrigin::root()));
|
||||
|
||||
assert!(Assets::balance(PezAssetId::get(), treasury_account()) > 0);
|
||||
assert!(Assets::balance(PezAssetId::get(), presale()) > 0);
|
||||
assert!(Assets::balance(PezAssetId::get(), founder()) > 0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn genesis_distribution_can_only_happen_once() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// First call should succeed
|
||||
assert_ok!(PezTreasury::do_genesis_distribution());
|
||||
|
||||
// Verify flag is set
|
||||
assert!(PezTreasury::genesis_distribution_done());
|
||||
|
||||
// Second call should fail
|
||||
assert_noop!(
|
||||
PezTreasury::do_genesis_distribution(),
|
||||
Error::<Test>::GenesisDistributionAlreadyDone
|
||||
);
|
||||
|
||||
// Verify balances didn't double
|
||||
let treasury_amount = 4_812_500_000 * 1_000_000_000_000u128;
|
||||
assert_pez_balance(treasury_account(), treasury_amount);
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 2. TREASURY INITIALIZATION TESTS
|
||||
// =============================================================================
|
||||
|
||||
#[test]
|
||||
fn initialize_treasury_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let start_block = System::block_number();
|
||||
|
||||
assert_ok!(PezTreasury::initialize_treasury(RuntimeOrigin::root()));
|
||||
|
||||
// Verify storage
|
||||
assert_eq!(PezTreasury::treasury_start_block(), Some(start_block));
|
||||
|
||||
let halving_info = PezTreasury::halving_info();
|
||||
assert_eq!(halving_info.current_period, 0);
|
||||
assert_eq!(halving_info.period_start_block, start_block);
|
||||
assert!(!halving_info.monthly_amount.is_zero());
|
||||
|
||||
// Verify next release month
|
||||
assert_eq!(PezTreasury::next_release_month(), 0);
|
||||
|
||||
// Verify event
|
||||
System::assert_has_event(
|
||||
Event::TreasuryInitialized {
|
||||
start_block,
|
||||
initial_monthly_amount: halving_info.monthly_amount,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn initialize_treasury_fails_if_already_initialized() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(PezTreasury::initialize_treasury(RuntimeOrigin::root()));
|
||||
|
||||
// Try to initialize again
|
||||
assert_noop!(
|
||||
PezTreasury::initialize_treasury(RuntimeOrigin::root()),
|
||||
Error::<Test>::TreasuryAlreadyInitialized
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn initialize_treasury_requires_root() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_noop!(
|
||||
PezTreasury::initialize_treasury(RuntimeOrigin::signed(alice())),
|
||||
pezsp_runtime::DispatchError::BadOrigin
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn initialize_treasury_calculates_correct_monthly_amount() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(PezTreasury::initialize_treasury(RuntimeOrigin::root()));
|
||||
|
||||
let halving_info = PezTreasury::halving_info();
|
||||
|
||||
// First period total = 96.25% / 2 = 48.125%
|
||||
let treasury_total = 4_812_500_000 * 1_000_000_000_000u128;
|
||||
let first_period = treasury_total / 2;
|
||||
let expected_monthly = first_period / 48; // 48 months
|
||||
|
||||
assert_eq!(halving_info.monthly_amount, expected_monthly);
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 3. MONTHLY RELEASE TESTS
|
||||
// =============================================================================
|
||||
|
||||
#[test]
|
||||
fn release_monthly_funds_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(PezTreasury::do_genesis_distribution());
|
||||
assert_ok!(PezTreasury::initialize_treasury(RuntimeOrigin::root()));
|
||||
|
||||
let initial_monthly = PezTreasury::halving_info().monthly_amount;
|
||||
let incentive_expected = initial_monthly * 75 / 100;
|
||||
let government_expected = initial_monthly - incentive_expected;
|
||||
|
||||
run_to_block(432_001);
|
||||
|
||||
assert_ok!(PezTreasury::release_monthly_funds(RuntimeOrigin::root()));
|
||||
|
||||
assert_pez_balance(PezTreasury::incentive_pot_account_id(), incentive_expected);
|
||||
assert_pez_balance(PezTreasury::government_pot_account_id(), government_expected);
|
||||
|
||||
assert_eq!(PezTreasury::next_release_month(), 1);
|
||||
|
||||
let halving_info = PezTreasury::halving_info();
|
||||
assert_eq!(halving_info.total_released, initial_monthly);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_monthly_funds_fails_if_not_initialized() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_noop!(
|
||||
PezTreasury::release_monthly_funds(RuntimeOrigin::root()),
|
||||
Error::<Test>::TreasuryNotInitialized
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_monthly_funds_fails_if_too_early() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(PezTreasury::do_genesis_distribution());
|
||||
assert_ok!(PezTreasury::initialize_treasury(RuntimeOrigin::root()));
|
||||
|
||||
// Try to release before time
|
||||
run_to_block(100);
|
||||
|
||||
assert_noop!(
|
||||
PezTreasury::release_monthly_funds(RuntimeOrigin::root()),
|
||||
Error::<Test>::ReleaseTooEarly
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_monthly_funds_fails_if_already_released() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(PezTreasury::do_genesis_distribution());
|
||||
assert_ok!(PezTreasury::initialize_treasury(RuntimeOrigin::root()));
|
||||
|
||||
run_to_block(432_001);
|
||||
assert_ok!(PezTreasury::release_monthly_funds(RuntimeOrigin::root()));
|
||||
|
||||
// Try to release same month again
|
||||
assert_noop!(
|
||||
PezTreasury::release_monthly_funds(RuntimeOrigin::root()),
|
||||
Error::<Test>::ReleaseTooEarly
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_monthly_funds_splits_correctly() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(PezTreasury::do_genesis_distribution());
|
||||
assert_ok!(PezTreasury::initialize_treasury(RuntimeOrigin::root()));
|
||||
|
||||
let monthly_amount = PezTreasury::halving_info().monthly_amount;
|
||||
|
||||
run_to_block(432_001);
|
||||
assert_ok!(PezTreasury::release_monthly_funds(RuntimeOrigin::root()));
|
||||
|
||||
let incentive_balance =
|
||||
Assets::balance(PezAssetId::get(), PezTreasury::incentive_pot_account_id());
|
||||
let government_balance =
|
||||
Assets::balance(PezAssetId::get(), PezTreasury::government_pot_account_id());
|
||||
|
||||
// 75% to incentive, 25% to government
|
||||
assert_eq!(incentive_balance, monthly_amount * 75 / 100);
|
||||
// lib.rs'deki mantıkla aynı olmalı (saturating_sub)
|
||||
let incentive_amount_calculated = monthly_amount * 75 / 100;
|
||||
assert_eq!(government_balance, monthly_amount - incentive_amount_calculated);
|
||||
|
||||
// Total should equal monthly amount
|
||||
assert_eq!(incentive_balance + government_balance, monthly_amount);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_monthly_releases_work() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(PezTreasury::do_genesis_distribution());
|
||||
assert_ok!(PezTreasury::initialize_treasury(RuntimeOrigin::root()));
|
||||
|
||||
let monthly_amount = PezTreasury::halving_info().monthly_amount;
|
||||
|
||||
// Release month 0
|
||||
run_to_block(432_001);
|
||||
assert_ok!(PezTreasury::release_monthly_funds(RuntimeOrigin::root()));
|
||||
assert_eq!(PezTreasury::next_release_month(), 1);
|
||||
|
||||
// Release month 1
|
||||
run_to_block(864_001);
|
||||
assert_ok!(PezTreasury::release_monthly_funds(RuntimeOrigin::root()));
|
||||
assert_eq!(PezTreasury::next_release_month(), 2);
|
||||
|
||||
// Release month 2
|
||||
run_to_block(1_296_001);
|
||||
assert_ok!(PezTreasury::release_monthly_funds(RuntimeOrigin::root()));
|
||||
assert_eq!(PezTreasury::next_release_month(), 3);
|
||||
|
||||
// Verify total released
|
||||
let halving_info = PezTreasury::halving_info();
|
||||
assert_eq!(halving_info.total_released, monthly_amount * 3);
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 4. HALVING LOGIC TESTS
|
||||
// =============================================================================
|
||||
|
||||
#[test]
|
||||
fn halving_occurs_after_48_months() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(PezTreasury::do_genesis_distribution());
|
||||
assert_ok!(PezTreasury::initialize_treasury(RuntimeOrigin::root()));
|
||||
|
||||
let initial_monthly = PezTreasury::halving_info().monthly_amount;
|
||||
|
||||
// Release 47 months (no halving yet)
|
||||
for month in 0..47 {
|
||||
run_to_block(1 + (month + 1) * 432_000 + 1);
|
||||
assert_ok!(PezTreasury::release_monthly_funds(RuntimeOrigin::root()));
|
||||
}
|
||||
|
||||
// Still period 0
|
||||
assert_eq!(PezTreasury::halving_info().current_period, 0);
|
||||
assert_eq!(PezTreasury::halving_info().monthly_amount, initial_monthly);
|
||||
|
||||
// Release 48th month - halving should occur
|
||||
run_to_block(1 + 48 * 432_000 + 1);
|
||||
assert_ok!(PezTreasury::release_monthly_funds(RuntimeOrigin::root()));
|
||||
|
||||
// Now in period 1 with halved amount
|
||||
let halving_info = PezTreasury::halving_info();
|
||||
assert_eq!(halving_info.current_period, 1);
|
||||
assert_eq!(halving_info.monthly_amount, initial_monthly / 2);
|
||||
|
||||
// Verify event
|
||||
System::assert_has_event(
|
||||
Event::NewHalvingPeriod { period: 1, new_monthly_amount: initial_monthly / 2 }.into(),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_halvings_work() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(PezTreasury::do_genesis_distribution());
|
||||
assert_ok!(PezTreasury::initialize_treasury(RuntimeOrigin::root()));
|
||||
|
||||
let initial_monthly = PezTreasury::halving_info().monthly_amount;
|
||||
|
||||
// First halving at month 48
|
||||
run_to_block(1 + 48 * 432_000 + 1);
|
||||
assert_ok!(PezTreasury::release_monthly_funds(RuntimeOrigin::root()));
|
||||
assert_eq!(PezTreasury::halving_info().current_period, 1);
|
||||
assert_eq!(PezTreasury::halving_info().monthly_amount, initial_monthly / 2);
|
||||
|
||||
// Second halving at month 96
|
||||
run_to_block(1 + 96 * 432_000 + 1);
|
||||
for _ in 49..=96 {
|
||||
assert_ok!(PezTreasury::release_monthly_funds(RuntimeOrigin::root()));
|
||||
}
|
||||
assert_eq!(PezTreasury::halving_info().current_period, 2);
|
||||
assert_eq!(PezTreasury::halving_info().monthly_amount, initial_monthly / 4);
|
||||
|
||||
// Third halving at month 144
|
||||
run_to_block(1 + 144 * 432_000 + 1);
|
||||
for _ in 97..=144 {
|
||||
assert_ok!(PezTreasury::release_monthly_funds(RuntimeOrigin::root()));
|
||||
}
|
||||
assert_eq!(PezTreasury::halving_info().current_period, 3);
|
||||
assert_eq!(PezTreasury::halving_info().monthly_amount, initial_monthly / 8);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn halving_period_start_block_updates() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(PezTreasury::do_genesis_distribution());
|
||||
assert_ok!(PezTreasury::initialize_treasury(RuntimeOrigin::root()));
|
||||
|
||||
let period_0_start = PezTreasury::halving_info().period_start_block;
|
||||
|
||||
// Trigger halving
|
||||
run_to_block(1 + 48 * 432_000 + 1);
|
||||
assert_ok!(PezTreasury::release_monthly_funds(RuntimeOrigin::root()));
|
||||
|
||||
let period_1_start = PezTreasury::halving_info().period_start_block;
|
||||
assert!(period_1_start > period_0_start);
|
||||
assert_eq!(period_1_start, System::block_number());
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 5. ERROR CASES
|
||||
// =============================================================================
|
||||
|
||||
#[test]
|
||||
fn insufficient_treasury_balance_error() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// Initialize without genesis distribution (treasury empty)
|
||||
assert_ok!(PezTreasury::initialize_treasury(RuntimeOrigin::root()));
|
||||
|
||||
run_to_block(432_001);
|
||||
|
||||
// This should fail due to insufficient balance
|
||||
assert_noop!(
|
||||
PezTreasury::release_monthly_funds(RuntimeOrigin::root()),
|
||||
Error::<Test>::InsufficientTreasuryBalance
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_requires_root_origin() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(PezTreasury::do_genesis_distribution());
|
||||
assert_ok!(PezTreasury::initialize_treasury(RuntimeOrigin::root()));
|
||||
|
||||
run_to_block(432_001);
|
||||
|
||||
assert_noop!(
|
||||
PezTreasury::release_monthly_funds(RuntimeOrigin::signed(alice())),
|
||||
pezsp_runtime::DispatchError::BadOrigin
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 6. EDGE CASES
|
||||
// =============================================================================
|
||||
|
||||
#[test]
|
||||
fn release_exactly_at_boundary_block_fails() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(PezTreasury::do_genesis_distribution());
|
||||
assert_ok!(PezTreasury::initialize_treasury(RuntimeOrigin::root()));
|
||||
|
||||
// Tam 432_000. blok (start_block=1 olduğu için) 431_999 blok geçti demektir.
|
||||
// Bu, 1 tam ay (432_000 blok) değildir.
|
||||
run_to_block(432_000);
|
||||
assert_noop!(
|
||||
PezTreasury::release_monthly_funds(RuntimeOrigin::root()),
|
||||
Error::<Test>::ReleaseTooEarly
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_one_block_before_boundary_fails() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(PezTreasury::do_genesis_distribution());
|
||||
assert_ok!(PezTreasury::initialize_treasury(RuntimeOrigin::root()));
|
||||
|
||||
run_to_block(432_000 - 1);
|
||||
assert_noop!(
|
||||
PezTreasury::release_monthly_funds(RuntimeOrigin::root()),
|
||||
Error::<Test>::ReleaseTooEarly
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skip_months_and_release() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(PezTreasury::do_genesis_distribution());
|
||||
assert_ok!(PezTreasury::initialize_treasury(RuntimeOrigin::root()));
|
||||
|
||||
// Skip directly to month 3
|
||||
run_to_block(1 + 3 * 432_000 + 1);
|
||||
|
||||
// Should release month 0
|
||||
assert_ok!(PezTreasury::release_monthly_funds(RuntimeOrigin::root()));
|
||||
assert_eq!(PezTreasury::next_release_month(), 1);
|
||||
|
||||
// Can still release subsequent months
|
||||
assert_ok!(PezTreasury::release_monthly_funds(RuntimeOrigin::root()));
|
||||
assert_eq!(PezTreasury::next_release_month(), 2);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn very_large_block_number() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(PezTreasury::do_genesis_distribution());
|
||||
assert_ok!(PezTreasury::initialize_treasury(RuntimeOrigin::root()));
|
||||
|
||||
// Jump to very large block number
|
||||
System::set_block_number(u64::MAX / 2);
|
||||
|
||||
// Should still be able to release (if months passed)
|
||||
// This tests overflow protection
|
||||
let result = PezTreasury::release_monthly_funds(RuntimeOrigin::root());
|
||||
// Result depends on whether enough months passed
|
||||
// Main point: no panic/overflow
|
||||
assert!(result.is_ok() || result.is_err());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_amount_division_protection() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// Initialize without any balance
|
||||
assert_ok!(PezTreasury::initialize_treasury(RuntimeOrigin::root()));
|
||||
|
||||
let halving_info = PezTreasury::halving_info();
|
||||
// Should not panic, should have some calculated amount
|
||||
assert!(!halving_info.monthly_amount.is_zero());
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 7. GETTER FUNCTIONS TESTS
|
||||
// =============================================================================
|
||||
|
||||
#[test]
|
||||
fn get_current_halving_info_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(PezTreasury::initialize_treasury(RuntimeOrigin::root()));
|
||||
|
||||
let info = PezTreasury::get_current_halving_info();
|
||||
assert_eq!(info.current_period, 0);
|
||||
assert!(!info.monthly_amount.is_zero());
|
||||
assert_eq!(info.total_released, 0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_incentive_pot_balance_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(PezTreasury::do_genesis_distribution());
|
||||
assert_ok!(PezTreasury::initialize_treasury(RuntimeOrigin::root()));
|
||||
|
||||
run_to_block(432_001);
|
||||
assert_ok!(PezTreasury::release_monthly_funds(RuntimeOrigin::root()));
|
||||
|
||||
let balance = PezTreasury::get_incentive_pot_balance();
|
||||
assert!(balance > 0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_government_pot_balance_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(PezTreasury::do_genesis_distribution());
|
||||
assert_ok!(PezTreasury::initialize_treasury(RuntimeOrigin::root()));
|
||||
|
||||
run_to_block(432_001);
|
||||
assert_ok!(PezTreasury::release_monthly_funds(RuntimeOrigin::root()));
|
||||
|
||||
let balance = PezTreasury::get_government_pot_balance();
|
||||
assert!(balance > 0);
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 8. ACCOUNT ID TESTS
|
||||
// =============================================================================
|
||||
|
||||
#[test]
|
||||
fn treasury_account_id_is_consistent() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let account1 = PezTreasury::treasury_account_id();
|
||||
let account2 = PezTreasury::treasury_account_id();
|
||||
assert_eq!(account1, account2);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pot_accounts_are_different() {
|
||||
new_test_ext().execute_with(|| {
|
||||
debug_pot_accounts();
|
||||
|
||||
let treasury = PezTreasury::treasury_account_id();
|
||||
let incentive = PezTreasury::incentive_pot_account_id();
|
||||
let government = PezTreasury::government_pot_account_id();
|
||||
|
||||
println!("\n=== Account IDs from Pallet ===");
|
||||
println!("Treasury: {:?}", treasury);
|
||||
println!("Incentive: {:?}", incentive);
|
||||
println!("Government: {:?}", government);
|
||||
println!("================================\n");
|
||||
|
||||
// Tüm üçü farklı olmalı
|
||||
assert_ne!(treasury, incentive, "Treasury and Incentive must be different");
|
||||
assert_ne!(treasury, government, "Treasury and Government must be different");
|
||||
assert_ne!(incentive, government, "Incentive and Government must be different");
|
||||
|
||||
println!("✓ All pot accounts are different!");
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 9. MONTHLY RELEASE STORAGE TESTS
|
||||
// =============================================================================
|
||||
|
||||
#[test]
|
||||
fn monthly_release_records_stored_correctly() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(PezTreasury::do_genesis_distribution());
|
||||
assert_ok!(PezTreasury::initialize_treasury(RuntimeOrigin::root()));
|
||||
|
||||
let monthly_amount = PezTreasury::halving_info().monthly_amount;
|
||||
let incentive_expected = monthly_amount * 75 / 100;
|
||||
let government_expected = monthly_amount - incentive_expected;
|
||||
|
||||
run_to_block(432_001);
|
||||
assert_ok!(PezTreasury::release_monthly_funds(RuntimeOrigin::root()));
|
||||
|
||||
// Verify monthly release record
|
||||
let release = PezTreasury::monthly_releases(0).unwrap();
|
||||
assert_eq!(release.month_index, 0);
|
||||
assert_eq!(release.amount_released, monthly_amount);
|
||||
assert_eq!(release.incentive_amount, incentive_expected);
|
||||
assert_eq!(release.government_amount, government_expected);
|
||||
assert_eq!(release.release_block, System::block_number());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_monthly_releases_stored_separately() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(PezTreasury::do_genesis_distribution());
|
||||
assert_ok!(PezTreasury::initialize_treasury(RuntimeOrigin::root()));
|
||||
|
||||
// Release month 0
|
||||
run_to_block(432_001);
|
||||
assert_ok!(PezTreasury::release_monthly_funds(RuntimeOrigin::root()));
|
||||
|
||||
// Release month 1
|
||||
run_to_block(864_001);
|
||||
assert_ok!(PezTreasury::release_monthly_funds(RuntimeOrigin::root()));
|
||||
|
||||
// Verify both records exist
|
||||
assert!(PezTreasury::monthly_releases(0).is_some());
|
||||
assert!(PezTreasury::monthly_releases(1).is_some());
|
||||
|
||||
let release_0 = PezTreasury::monthly_releases(0).unwrap();
|
||||
let release_1 = PezTreasury::monthly_releases(1).unwrap();
|
||||
|
||||
assert_eq!(release_0.month_index, 0);
|
||||
assert_eq!(release_1.month_index, 1);
|
||||
assert_ne!(release_0.release_block, release_1.release_block);
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 10. INTEGRATION TESTS
|
||||
// =============================================================================
|
||||
|
||||
#[test]
|
||||
fn full_lifecycle_test() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// 1. Genesis distribution
|
||||
assert_ok!(PezTreasury::do_genesis_distribution());
|
||||
let treasury_initial = Assets::balance(PezAssetId::get(), treasury_account());
|
||||
assert!(treasury_initial > 0);
|
||||
|
||||
// 2. Initialize treasury
|
||||
assert_ok!(PezTreasury::initialize_treasury(RuntimeOrigin::root()));
|
||||
let monthly_amount = PezTreasury::halving_info().monthly_amount;
|
||||
|
||||
// 3. Release first month
|
||||
run_to_block(432_001);
|
||||
assert_ok!(PezTreasury::release_monthly_funds(RuntimeOrigin::root()));
|
||||
|
||||
let treasury_after_month_0 = Assets::balance(PezAssetId::get(), treasury_account());
|
||||
assert_eq!(treasury_initial - treasury_after_month_0, monthly_amount);
|
||||
|
||||
// 4. Release multiple months
|
||||
for month in 1..10 {
|
||||
run_to_block(1 + (month + 1) * 432_000 + 1);
|
||||
assert_ok!(PezTreasury::release_monthly_funds(RuntimeOrigin::root()));
|
||||
}
|
||||
|
||||
// 5. Verify cumulative release
|
||||
let halving_info = PezTreasury::halving_info();
|
||||
assert_eq!(halving_info.total_released, monthly_amount * 10);
|
||||
|
||||
// 6. Verify treasury balance decreased correctly
|
||||
let treasury_after_10_months = Assets::balance(PezAssetId::get(), treasury_account());
|
||||
assert_eq!(treasury_initial - treasury_after_10_months, monthly_amount * 10);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_halving_cycle_test() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(PezTreasury::do_genesis_distribution());
|
||||
assert_ok!(PezTreasury::initialize_treasury(RuntimeOrigin::root()));
|
||||
|
||||
let initial_monthly = PezTreasury::halving_info().monthly_amount;
|
||||
let mut cumulative_released = 0u128;
|
||||
|
||||
// Period 0: 48 months at initial rate
|
||||
for month in 0..48 {
|
||||
run_to_block(1 + (month + 1) * 432_000 + 1);
|
||||
assert_ok!(PezTreasury::release_monthly_funds(RuntimeOrigin::root()));
|
||||
|
||||
if month < 47 {
|
||||
cumulative_released += initial_monthly;
|
||||
} else {
|
||||
// 48. sürümde (index 47) halving tetiklenir ve yarı tutar kullanılır
|
||||
cumulative_released += initial_monthly / 2;
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(PezTreasury::halving_info().current_period, 1);
|
||||
assert_eq!(PezTreasury::halving_info().monthly_amount, initial_monthly / 2);
|
||||
|
||||
// Period 1: 48 months at half rate
|
||||
for month in 48..96 {
|
||||
run_to_block(1 + (month + 1) * 432_000 + 1);
|
||||
assert_ok!(PezTreasury::release_monthly_funds(RuntimeOrigin::root()));
|
||||
|
||||
if month < 95 {
|
||||
cumulative_released += initial_monthly / 2;
|
||||
} else {
|
||||
// 96. sürümde (index 95) ikinci halving tetiklenir
|
||||
cumulative_released += initial_monthly / 4;
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(PezTreasury::halving_info().current_period, 2);
|
||||
assert_eq!(PezTreasury::halving_info().monthly_amount, initial_monthly / 4);
|
||||
|
||||
// Verify total released matches expectation
|
||||
assert_eq!(PezTreasury::halving_info().total_released, cumulative_released);
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 11. PRECISION AND ROUNDING TESTS
|
||||
// =============================================================================
|
||||
|
||||
#[test]
|
||||
fn division_rounding_is_consistent() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(PezTreasury::do_genesis_distribution());
|
||||
assert_ok!(PezTreasury::initialize_treasury(RuntimeOrigin::root()));
|
||||
|
||||
let monthly_amount = PezTreasury::halving_info().monthly_amount;
|
||||
let incentive_amount = monthly_amount * 75 / 100;
|
||||
let government_amount = monthly_amount - incentive_amount;
|
||||
|
||||
// Verify no rounding loss
|
||||
assert_eq!(incentive_amount + government_amount, monthly_amount);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn halving_precision_maintained() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(PezTreasury::initialize_treasury(RuntimeOrigin::root()));
|
||||
|
||||
let initial = PezTreasury::halving_info().monthly_amount;
|
||||
|
||||
// Trigger halving
|
||||
run_to_block(1 + 48 * 432_000 + 1);
|
||||
assert_ok!(PezTreasury::do_genesis_distribution());
|
||||
assert_ok!(PezTreasury::release_monthly_funds(RuntimeOrigin::root()));
|
||||
|
||||
let after_halving = PezTreasury::halving_info().monthly_amount;
|
||||
|
||||
// Check halving is exactly half (no precision loss)
|
||||
assert_eq!(after_halving, initial / 2);
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 12. EVENT EMISSION TESTS
|
||||
// =============================================================================
|
||||
|
||||
#[test]
|
||||
fn all_events_emitted_correctly() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// Genesis distribution event
|
||||
assert_ok!(PezTreasury::do_genesis_distribution());
|
||||
assert!(System::events().iter().any(|e| matches!(
|
||||
e.event,
|
||||
RuntimeEvent::PezTreasury(Event::GenesisDistributionCompleted { .. })
|
||||
)));
|
||||
|
||||
// Treasury initialized event
|
||||
assert_ok!(PezTreasury::initialize_treasury(RuntimeOrigin::root()));
|
||||
assert!(System::events().iter().any(|e| matches!(
|
||||
e.event,
|
||||
RuntimeEvent::PezTreasury(Event::TreasuryInitialized { .. })
|
||||
)));
|
||||
|
||||
// Monthly funds released event
|
||||
run_to_block(432_001);
|
||||
assert_ok!(PezTreasury::release_monthly_funds(RuntimeOrigin::root()));
|
||||
assert!(System::events().iter().any(|e| matches!(
|
||||
e.event,
|
||||
RuntimeEvent::PezTreasury(Event::MonthlyFundsReleased { .. })
|
||||
)));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn halving_event_emitted_at_correct_time() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(PezTreasury::do_genesis_distribution());
|
||||
assert_ok!(PezTreasury::initialize_treasury(RuntimeOrigin::root()));
|
||||
|
||||
// Clear existing events
|
||||
System::reset_events();
|
||||
|
||||
// Release up to halving point
|
||||
run_to_block(1 + 48 * 432_000 + 1);
|
||||
assert_ok!(PezTreasury::release_monthly_funds(RuntimeOrigin::root()));
|
||||
|
||||
// Verify halving event emitted
|
||||
assert!(System::events().iter().any(|e| matches!(
|
||||
e.event,
|
||||
RuntimeEvent::PezTreasury(Event::NewHalvingPeriod { period: 1, .. })
|
||||
)));
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 13. STRESS TESTS
|
||||
// =============================================================================
|
||||
|
||||
#[test]
|
||||
fn many_consecutive_releases() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(PezTreasury::do_genesis_distribution());
|
||||
assert_ok!(PezTreasury::initialize_treasury(RuntimeOrigin::root()));
|
||||
|
||||
// Release 100 months consecutively
|
||||
for month in 0..100 {
|
||||
run_to_block(1 + (month + 1) * 432_000 + 1);
|
||||
assert_ok!(PezTreasury::release_monthly_funds(RuntimeOrigin::root()));
|
||||
}
|
||||
|
||||
// Verify state is consistent
|
||||
assert_eq!(PezTreasury::next_release_month(), 100);
|
||||
|
||||
// Should be in period 2 (after 2 halvings at months 48 and 96)
|
||||
assert_eq!(PezTreasury::halving_info().current_period, 2);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn treasury_never_goes_negative() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(PezTreasury::do_genesis_distribution());
|
||||
assert_ok!(PezTreasury::initialize_treasury(RuntimeOrigin::root()));
|
||||
|
||||
let _initial_balance = Assets::balance(PezAssetId::get(), treasury_account()); // FIXED: Prefixed with underscore
|
||||
|
||||
// Try to release many months
|
||||
for month in 0..200 {
|
||||
run_to_block(1 + (month + 1) * 432_000 + 1);
|
||||
|
||||
let before_balance = Assets::balance(PezAssetId::get(), treasury_account());
|
||||
|
||||
let result = PezTreasury::release_monthly_funds(RuntimeOrigin::root());
|
||||
|
||||
if result.is_ok() {
|
||||
let after_balance = Assets::balance(PezAssetId::get(), treasury_account());
|
||||
// Balance should decrease or stay the same, never increase
|
||||
assert!(after_balance <= before_balance);
|
||||
// Balance should never go below zero
|
||||
assert!(after_balance >= 0);
|
||||
} else {
|
||||
// If release fails, balance should be unchanged
|
||||
assert_eq!(before_balance, Assets::balance(PezAssetId::get(), treasury_account()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 14. BOUNDARY CONDITION TESTS
|
||||
// =============================================================================
|
||||
|
||||
#[test]
|
||||
fn first_block_initialization() {
|
||||
new_test_ext().execute_with(|| {
|
||||
System::set_block_number(1);
|
||||
assert_ok!(PezTreasury::initialize_treasury(RuntimeOrigin::root()));
|
||||
assert_eq!(PezTreasury::treasury_start_block(), Some(1));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn last_month_of_period_before_halving() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(PezTreasury::do_genesis_distribution());
|
||||
assert_ok!(PezTreasury::initialize_treasury(RuntimeOrigin::root()));
|
||||
|
||||
let initial_amount = PezTreasury::halving_info().monthly_amount;
|
||||
|
||||
// Release month 47 (last before halving)
|
||||
run_to_block(1 + 47 * 432_000 + 1);
|
||||
assert_ok!(PezTreasury::release_monthly_funds(RuntimeOrigin::root()));
|
||||
|
||||
// Should still be in period 0
|
||||
assert_eq!(PezTreasury::halving_info().current_period, 0);
|
||||
assert_eq!(PezTreasury::halving_info().monthly_amount, initial_amount);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_month_after_halving() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(PezTreasury::do_genesis_distribution());
|
||||
assert_ok!(PezTreasury::initialize_treasury(RuntimeOrigin::root()));
|
||||
|
||||
let initial_amount = PezTreasury::halving_info().monthly_amount;
|
||||
|
||||
// Trigger halving at month 48
|
||||
run_to_block(1 + 48 * 432_000 + 1);
|
||||
assert_ok!(PezTreasury::release_monthly_funds(RuntimeOrigin::root()));
|
||||
|
||||
// Should be in period 1 with halved amount
|
||||
assert_eq!(PezTreasury::halving_info().current_period, 1);
|
||||
assert_eq!(PezTreasury::halving_info().monthly_amount, initial_amount / 2);
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 15. MATHEMATICAL CORRECTNESS TESTS
|
||||
// =============================================================================
|
||||
|
||||
#[test]
|
||||
fn total_supply_equals_sum_of_allocations() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(PezTreasury::do_genesis_distribution());
|
||||
|
||||
let treasury = Assets::balance(PezAssetId::get(), treasury_account());
|
||||
let presale_acc = Assets::balance(PezAssetId::get(), presale());
|
||||
let founder_acc = Assets::balance(PezAssetId::get(), founder());
|
||||
|
||||
let total = treasury + presale_acc + founder_acc;
|
||||
let expected_total = 5_000_000_000 * 1_000_000_000_000u128;
|
||||
|
||||
assert_eq!(total, expected_total);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn percentage_allocations_correct() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(PezTreasury::do_genesis_distribution());
|
||||
|
||||
let total_supply = 5_000_000_000 * 1_000_000_000_000u128;
|
||||
let treasury = Assets::balance(PezAssetId::get(), treasury_account());
|
||||
let presale_acc = Assets::balance(PezAssetId::get(), presale());
|
||||
let founder_acc = Assets::balance(PezAssetId::get(), founder());
|
||||
|
||||
assert_eq!(treasury, total_supply * 9625 / 10000);
|
||||
assert_eq!(presale_acc, total_supply * 1875 / 100000);
|
||||
assert_eq!(founder_acc, total_supply * 1875 / 100000);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_period_total_is_half_of_treasury() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(PezTreasury::do_genesis_distribution());
|
||||
assert_ok!(PezTreasury::initialize_treasury(RuntimeOrigin::root()));
|
||||
|
||||
let monthly_amount = PezTreasury::halving_info().monthly_amount;
|
||||
let first_period_total = monthly_amount * 48;
|
||||
|
||||
let treasury_allocation = 4_812_500_000 * 1_000_000_000_000u128;
|
||||
let expected_first_period = treasury_allocation / 2;
|
||||
|
||||
let diff = expected_first_period.saturating_sub(first_period_total);
|
||||
// Kalanların toplamı 48'den az olmalı (her ay en fazla 1 birim kalan)
|
||||
assert!(diff < 48, "Rounding error too large: {}", diff);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn geometric_series_sum_validates() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(PezTreasury::initialize_treasury(RuntimeOrigin::root()));
|
||||
|
||||
let initial_monthly = PezTreasury::halving_info().monthly_amount;
|
||||
|
||||
// Sum of geometric series: a(1 - r^n) / (1 - r)
|
||||
// For halving: first_period * (1 - 0.5^n) / 0.5
|
||||
// With infinite halvings approaches: first_period * 2
|
||||
|
||||
let first_period_total = initial_monthly * 48;
|
||||
let treasury_allocation = 4_812_500_000 * 1_000_000_000_000u128;
|
||||
|
||||
// After infinite halvings, total distributed = treasury_allocation
|
||||
// first_period_total * 2 = treasury_allocation
|
||||
let diff = treasury_allocation.saturating_sub(first_period_total * 2);
|
||||
// Kalanların toplamı (2 ile çarpılmış) 96'dan az olmalı
|
||||
assert!(diff < 96, "Rounding error too large: {}", diff);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
|
||||
//! Autogenerated weights for `pezpallet_pez_treasury`
|
||||
//!
|
||||
//! THIS FILE WAS AUTO-GENERATED USING THE BIZINIKIWI BENCHMARK CLI VERSION 32.0.0
|
||||
//! DATE: 2025-12-08, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
|
||||
//! WORST CASE MAP SIZE: `1000000`
|
||||
//! HOSTNAME: `MamostePC`, CPU: `11th Gen Intel(R) Core(TM) i9-11950H @ 2.60GHz`
|
||||
//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024
|
||||
|
||||
// Executed Command:
|
||||
// ./target/release/frame-omni-bencher
|
||||
// v1
|
||||
// benchmark
|
||||
// pallet
|
||||
// --runtime
|
||||
// ./target/release/wbuild/asset-hub-pezkuwichain-runtime/asset_hub_pezkuwichain_runtime.wasm
|
||||
// --pallet
|
||||
// pezpallet_pez_treasury
|
||||
// --extrinsic
|
||||
// *
|
||||
// --steps
|
||||
// 50
|
||||
// --repeat
|
||||
// 20
|
||||
// --output
|
||||
// ./pezcumulus/teyrchains/pallets/pez-treasury/src/weights.rs
|
||||
|
||||
#![cfg_attr(rustfmt, rustfmt_skip)]
|
||||
#![allow(unused_parens)]
|
||||
#![allow(unused_imports)]
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use pezframe_support::{traits::Get, weights::Weight};
|
||||
use core::marker::PhantomData;
|
||||
|
||||
/// Weight functions needed for `pezpallet_pez_treasury`.
|
||||
pub trait WeightInfo {
|
||||
fn initialize_treasury() -> Weight;
|
||||
fn force_genesis_distribution() -> Weight;
|
||||
fn release_monthly_funds() -> Weight;
|
||||
}
|
||||
|
||||
/// Weight functions for `pezpallet_pez_treasury`.
|
||||
pub struct BizinikiwiWeight<T>(PhantomData<T>);
|
||||
impl<T: pezframe_system::Config> WeightInfo for BizinikiwiWeight<T> {
|
||||
/// Storage: `PezTreasury::TreasuryStartBlock` (r:1 w:1)
|
||||
/// Proof: `PezTreasury::TreasuryStartBlock` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
|
||||
/// Storage: `PezTreasury::NextReleaseMonth` (r:0 w:1)
|
||||
/// Proof: `PezTreasury::NextReleaseMonth` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
|
||||
/// Storage: `PezTreasury::HalvingInfo` (r:0 w:1)
|
||||
/// Proof: `PezTreasury::HalvingInfo` (`max_values`: Some(1), `max_size`: Some(40), added: 535, mode: `MaxEncodedLen`)
|
||||
fn initialize_treasury() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `3`
|
||||
// Estimated: `1489`
|
||||
// Minimum execution time: 7_724_000 picoseconds.
|
||||
Weight::from_parts(8_079_000, 0)
|
||||
.saturating_add(Weight::from_parts(0, 1489))
|
||||
.saturating_add(T::DbWeight::get().reads(1))
|
||||
.saturating_add(T::DbWeight::get().writes(3))
|
||||
}
|
||||
/// Storage: `PezTreasury::GenesisDistributionDone` (r:1 w:1)
|
||||
/// Proof: `PezTreasury::GenesisDistributionDone` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Assets::Asset` (r:1 w:1)
|
||||
/// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Assets::Account` (r:3 w:3)
|
||||
/// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
|
||||
/// Storage: `System::Account` (r:3 w:3)
|
||||
/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
|
||||
fn force_genesis_distribution() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `489`
|
||||
// Estimated: `8817`
|
||||
// Minimum execution time: 62_294_000 picoseconds.
|
||||
Weight::from_parts(64_751_000, 0)
|
||||
.saturating_add(Weight::from_parts(0, 8817))
|
||||
.saturating_add(T::DbWeight::get().reads(8))
|
||||
.saturating_add(T::DbWeight::get().writes(8))
|
||||
}
|
||||
/// Storage: `PezTreasury::TreasuryStartBlock` (r:1 w:0)
|
||||
/// Proof: `PezTreasury::TreasuryStartBlock` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
|
||||
/// Storage: `PezTreasury::NextReleaseMonth` (r:1 w:1)
|
||||
/// Proof: `PezTreasury::NextReleaseMonth` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
|
||||
/// Storage: `PezTreasury::MonthlyReleases` (r:1 w:1)
|
||||
/// Proof: `PezTreasury::MonthlyReleases` (`max_values`: None, `max_size`: Some(76), added: 2551, mode: `MaxEncodedLen`)
|
||||
/// Storage: `PezTreasury::HalvingInfo` (r:1 w:1)
|
||||
/// Proof: `PezTreasury::HalvingInfo` (`max_values`: Some(1), `max_size`: Some(40), added: 535, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Assets::Asset` (r:1 w:1)
|
||||
/// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Assets::Account` (r:3 w:3)
|
||||
/// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
|
||||
/// Storage: `AssetsFreezer::FrozenBalances` (r:1 w:0)
|
||||
/// Proof: `AssetsFreezer::FrozenBalances` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
|
||||
/// Storage: `System::Account` (r:2 w:2)
|
||||
/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
|
||||
fn release_monthly_funds() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `701`
|
||||
// Estimated: `8817`
|
||||
// Minimum execution time: 103_894_000 picoseconds.
|
||||
Weight::from_parts(109_089_000, 0)
|
||||
.saturating_add(Weight::from_parts(0, 8817))
|
||||
.saturating_add(T::DbWeight::get().reads(11))
|
||||
.saturating_add(T::DbWeight::get().writes(9))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
[package]
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
name = "pezcumulus-ping"
|
||||
version = "0.7.0"
|
||||
license = "Apache-2.0"
|
||||
description = "Ping Pallet for Pezcumulus XCM/UMP testing."
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
codec = { features = ["derive"], workspace = true }
|
||||
scale-info = { features = ["derive"], workspace = true }
|
||||
|
||||
pezframe-support = { workspace = true }
|
||||
pezframe-system = { workspace = true }
|
||||
pezsp-runtime = { workspace = true }
|
||||
|
||||
xcm = { workspace = true }
|
||||
|
||||
pezcumulus-pezpallet-xcm = { workspace = true }
|
||||
pezcumulus-primitives-core = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = [
|
||||
"codec/std",
|
||||
"pezcumulus-pezpallet-xcm/std",
|
||||
"pezcumulus-primitives-core/std",
|
||||
"pezframe-support/std",
|
||||
"pezframe-system/std",
|
||||
"scale-info/std",
|
||||
"pezsp-runtime/std",
|
||||
"xcm/std",
|
||||
]
|
||||
try-runtime = [
|
||||
"pezcumulus-pezpallet-xcm/try-runtime",
|
||||
"pezframe-support/try-runtime",
|
||||
"pezframe-system/try-runtime",
|
||||
"pezsp-runtime/try-runtime",
|
||||
]
|
||||
runtime-benchmarks = [
|
||||
"pezcumulus-pezpallet-xcm/runtime-benchmarks",
|
||||
"pezcumulus-primitives-core/runtime-benchmarks",
|
||||
"pezframe-support/runtime-benchmarks",
|
||||
"pezframe-system/runtime-benchmarks",
|
||||
"pezsp-runtime/runtime-benchmarks",
|
||||
"xcm/runtime-benchmarks",
|
||||
]
|
||||
@@ -0,0 +1,249 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// This file is part of Pezcumulus.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Pallet to spam the XCM/UMP.
|
||||
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
use alloc::{vec, vec::Vec};
|
||||
use cumulus_pallet_xcm::{ensure_sibling_para, Origin as CumulusOrigin};
|
||||
use cumulus_primitives_core::ParaId;
|
||||
use pezframe_support::{parameter_types, BoundedVec};
|
||||
use pezframe_system::Config as SystemConfig;
|
||||
use pezsp_runtime::traits::Saturating;
|
||||
use xcm::latest::prelude::*;
|
||||
|
||||
pub use pallet::*;
|
||||
|
||||
parameter_types! {
|
||||
const MaxTeyrchains: u32 = 100;
|
||||
const MaxPayloadSize: u32 = 1024;
|
||||
}
|
||||
|
||||
#[pezframe_support::pallet]
|
||||
pub mod pallet {
|
||||
use super::*;
|
||||
use pezframe_support::pezpallet_prelude::*;
|
||||
use pezframe_system::pezpallet_prelude::*;
|
||||
|
||||
#[pallet::pallet]
|
||||
pub struct Pallet<T>(_);
|
||||
|
||||
/// The module configuration trait.
|
||||
#[pallet::config]
|
||||
pub trait Config: pezframe_system::Config {
|
||||
/// The overarching event type.
|
||||
#[allow(deprecated)]
|
||||
type RuntimeEvent: From<Event<Self>> + IsType<<Self as pezframe_system::Config>::RuntimeEvent>;
|
||||
|
||||
type RuntimeOrigin: From<<Self as SystemConfig>::RuntimeOrigin>
|
||||
+ Into<Result<CumulusOrigin, <Self as Config>::RuntimeOrigin>>;
|
||||
|
||||
/// The overarching call type; we assume sibling chains use the same type.
|
||||
type RuntimeCall: From<Call<Self>> + Encode;
|
||||
|
||||
type XcmSender: SendXcm;
|
||||
}
|
||||
|
||||
/// The target teyrchains to ping.
|
||||
#[pallet::storage]
|
||||
pub(super) type Targets<T: Config> = StorageValue<
|
||||
_,
|
||||
BoundedVec<(ParaId, BoundedVec<u8, MaxPayloadSize>), MaxTeyrchains>,
|
||||
ValueQuery,
|
||||
>;
|
||||
|
||||
/// The total number of pings sent.
|
||||
#[pallet::storage]
|
||||
pub(super) type PingCount<T: Config> = StorageValue<_, u32, ValueQuery>;
|
||||
|
||||
/// The sent pings.
|
||||
#[pallet::storage]
|
||||
pub(super) type Pings<T: Config> =
|
||||
StorageMap<_, Blake2_128Concat, u32, BlockNumberFor<T>, OptionQuery>;
|
||||
|
||||
#[pallet::event]
|
||||
#[pallet::generate_deposit(pub(super) fn deposit_event)]
|
||||
pub enum Event<T: Config> {
|
||||
PingSent(ParaId, u32, Vec<u8>, XcmHash, Assets),
|
||||
Pinged(ParaId, u32, Vec<u8>),
|
||||
PongSent(ParaId, u32, Vec<u8>, XcmHash, Assets),
|
||||
Ponged(ParaId, u32, Vec<u8>, BlockNumberFor<T>),
|
||||
ErrorSendingPing(SendError, ParaId, u32, Vec<u8>),
|
||||
ErrorSendingPong(SendError, ParaId, u32, Vec<u8>),
|
||||
UnknownPong(ParaId, u32, Vec<u8>),
|
||||
}
|
||||
|
||||
#[pallet::error]
|
||||
pub enum Error<T> {
|
||||
/// Too many teyrchains have been added as a target.
|
||||
TooManyTargets,
|
||||
/// The payload provided is too large, limit is 1024 bytes.
|
||||
PayloadTooLarge,
|
||||
}
|
||||
|
||||
#[pallet::hooks]
|
||||
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
|
||||
fn on_finalize(n: BlockNumberFor<T>) {
|
||||
for (para, payload) in Targets::<T>::get().into_iter() {
|
||||
let seq = PingCount::<T>::mutate(|seq| {
|
||||
*seq += 1;
|
||||
*seq
|
||||
});
|
||||
match send_xcm::<T::XcmSender>(
|
||||
(Parent, Junction::Teyrchain(para.into())).into(),
|
||||
Xcm(vec![Transact {
|
||||
origin_kind: OriginKind::Native,
|
||||
call: <T as Config>::RuntimeCall::from(Call::<T>::ping {
|
||||
seq,
|
||||
payload: payload.clone().to_vec(),
|
||||
})
|
||||
.encode()
|
||||
.into(),
|
||||
fallback_max_weight: None,
|
||||
}]),
|
||||
) {
|
||||
Ok((hash, cost)) => {
|
||||
Pings::<T>::insert(seq, n);
|
||||
Self::deposit_event(Event::PingSent(
|
||||
para,
|
||||
seq,
|
||||
payload.to_vec(),
|
||||
hash,
|
||||
cost,
|
||||
));
|
||||
},
|
||||
Err(e) => {
|
||||
Self::deposit_event(Event::ErrorSendingPing(
|
||||
e,
|
||||
para,
|
||||
seq,
|
||||
payload.to_vec(),
|
||||
));
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[pallet::call]
|
||||
impl<T: Config> Pallet<T> {
|
||||
#[pallet::call_index(0)]
|
||||
#[pallet::weight({0})]
|
||||
pub fn start(origin: OriginFor<T>, para: ParaId, payload: Vec<u8>) -> DispatchResult {
|
||||
ensure_root(origin)?;
|
||||
let payload = BoundedVec::<u8, MaxPayloadSize>::try_from(payload)
|
||||
.map_err(|_| Error::<T>::PayloadTooLarge)?;
|
||||
Targets::<T>::try_mutate(|t| {
|
||||
t.try_push((para, payload)).map_err(|_| Error::<T>::TooManyTargets)
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[pallet::call_index(1)]
|
||||
#[pallet::weight({0})]
|
||||
pub fn start_many(
|
||||
origin: OriginFor<T>,
|
||||
para: ParaId,
|
||||
count: u32,
|
||||
payload: Vec<u8>,
|
||||
) -> DispatchResult {
|
||||
ensure_root(origin)?;
|
||||
let bounded_payload = BoundedVec::<u8, MaxPayloadSize>::try_from(payload)
|
||||
.map_err(|_| Error::<T>::PayloadTooLarge)?;
|
||||
for _ in 0..count {
|
||||
Targets::<T>::try_mutate(|t| {
|
||||
t.try_push((para, bounded_payload.clone()))
|
||||
.map_err(|_| Error::<T>::TooManyTargets)
|
||||
})?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[pallet::call_index(2)]
|
||||
#[pallet::weight({0})]
|
||||
pub fn stop(origin: OriginFor<T>, para: ParaId) -> DispatchResult {
|
||||
ensure_root(origin)?;
|
||||
Targets::<T>::mutate(|t| {
|
||||
if let Some(p) = t.iter().position(|(p, _)| p == ¶) {
|
||||
t.swap_remove(p);
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[pallet::call_index(3)]
|
||||
#[pallet::weight({0})]
|
||||
pub fn stop_all(origin: OriginFor<T>, maybe_para: Option<ParaId>) -> DispatchResult {
|
||||
ensure_root(origin)?;
|
||||
if let Some(para) = maybe_para {
|
||||
Targets::<T>::mutate(|t| t.retain(|&(x, _)| x != para));
|
||||
} else {
|
||||
Targets::<T>::kill();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[pallet::call_index(4)]
|
||||
#[pallet::weight({0})]
|
||||
pub fn ping(origin: OriginFor<T>, seq: u32, payload: Vec<u8>) -> DispatchResult {
|
||||
// Only accept pings from other chains.
|
||||
let para = ensure_sibling_para(<T as Config>::RuntimeOrigin::from(origin))?;
|
||||
|
||||
Self::deposit_event(Event::Pinged(para, seq, payload.clone()));
|
||||
match send_xcm::<T::XcmSender>(
|
||||
(Parent, Junction::Teyrchain(para.into())).into(),
|
||||
Xcm(vec![Transact {
|
||||
origin_kind: OriginKind::Native,
|
||||
call: <T as Config>::RuntimeCall::from(Call::<T>::pong {
|
||||
seq,
|
||||
payload: payload.clone(),
|
||||
})
|
||||
.encode()
|
||||
.into(),
|
||||
fallback_max_weight: None,
|
||||
}]),
|
||||
) {
|
||||
Ok((hash, cost)) =>
|
||||
Self::deposit_event(Event::PongSent(para, seq, payload, hash, cost)),
|
||||
Err(e) => Self::deposit_event(Event::ErrorSendingPong(e, para, seq, payload)),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[pallet::call_index(5)]
|
||||
#[pallet::weight({0})]
|
||||
pub fn pong(origin: OriginFor<T>, seq: u32, payload: Vec<u8>) -> DispatchResult {
|
||||
// Only accept pings from other chains.
|
||||
let para = ensure_sibling_para(<T as Config>::RuntimeOrigin::from(origin))?;
|
||||
|
||||
if let Some(sent_at) = Pings::<T>::take(seq) {
|
||||
Self::deposit_event(Event::Ponged(
|
||||
para,
|
||||
seq,
|
||||
payload,
|
||||
pezframe_system::Pallet::<T>::block_number().saturating_sub(sent_at),
|
||||
));
|
||||
} else {
|
||||
// Pong received for a ping we apparently didn't send?!
|
||||
Self::deposit_event(Event::UnknownPong(para, seq, payload));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
[package]
|
||||
name = "pezpallet-presale"
|
||||
version = "1.0.0"
|
||||
description = "PEZ token presale pallet - accepts wUSDT, distributes PEZ"
|
||||
authors.workspace = true
|
||||
homepage.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
publish = false
|
||||
repository.workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
||||
[dependencies]
|
||||
codec = { package = "parity-scale-codec", version = "3.6.12", default-features = false, features = [
|
||||
"derive",
|
||||
"max-encoded-len",
|
||||
] }
|
||||
log = { default-features = false, workspace = true }
|
||||
scale-info = { default-features = false, features = [
|
||||
"derive",
|
||||
], workspace = true }
|
||||
serde = { features = ["alloc", "derive"], workspace = true }
|
||||
|
||||
pezframe-benchmarking = { optional = true, workspace = true }
|
||||
pezframe-support = { default-features = false, workspace = true }
|
||||
pezframe-system = { default-features = false, workspace = true }
|
||||
pezsp-runtime = { default-features = false, workspace = true }
|
||||
pezsp-std = { default-features = false, workspace = true }
|
||||
|
||||
pezpallet-assets = { default-features = false, workspace = true }
|
||||
pezpallet-balances = { default-features = false, workspace = true, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
pezpallet-balances = { workspace = true }
|
||||
pezsp-core = { workspace = true }
|
||||
pezsp-io = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = [
|
||||
"codec/std",
|
||||
"pezframe-benchmarking?/std",
|
||||
"pezframe-support/std",
|
||||
"pezframe-system/std",
|
||||
"log/std",
|
||||
"pezpallet-assets/std",
|
||||
"pezpallet-balances?/std",
|
||||
"scale-info/std",
|
||||
"serde/std",
|
||||
"pezsp-core/std",
|
||||
"pezsp-io/std",
|
||||
"pezsp-runtime/std",
|
||||
"pezsp-std/std",
|
||||
]
|
||||
runtime-benchmarks = [
|
||||
"pezframe-benchmarking/runtime-benchmarks",
|
||||
"pezframe-support/runtime-benchmarks",
|
||||
"pezframe-system/runtime-benchmarks",
|
||||
"pezpallet-assets/runtime-benchmarks",
|
||||
"pezpallet-balances",
|
||||
"pezpallet-balances?/runtime-benchmarks",
|
||||
"pezsp-io/runtime-benchmarks",
|
||||
"pezsp-runtime/runtime-benchmarks",
|
||||
]
|
||||
try-runtime = [
|
||||
"pezframe-support/try-runtime",
|
||||
"pezframe-system/try-runtime",
|
||||
"pezpallet-assets/try-runtime",
|
||||
"pezpallet-balances?/try-runtime",
|
||||
"pezsp-runtime/try-runtime",
|
||||
]
|
||||
@@ -0,0 +1,459 @@
|
||||
//! Benchmarking setup for pezpallet-presale
|
||||
//!
|
||||
//! Complete benchmarks for all presale operations including:
|
||||
//! - create_presale, cancel_presale, add_to_whitelist
|
||||
//! - contribute, refund, claim_vested
|
||||
//! - finalize_presale (with O(N) contributor loop)
|
||||
//! - refund_cancelled_presale, batch_refund_failed_presale
|
||||
|
||||
#![cfg(feature = "runtime-benchmarks")]
|
||||
|
||||
use super::*;
|
||||
#[allow(unused)]
|
||||
use crate::Pallet as Presale;
|
||||
use pezframe_benchmarking::v2::*;
|
||||
use pezframe_support::traits::fungibles::{Create, Mutate};
|
||||
use pezframe_system::RawOrigin;
|
||||
|
||||
/// Helper trait for benchmark asset setup
|
||||
pub trait BenchmarkHelper<AssetId, AccountId> {
|
||||
/// Create an asset ID from seed
|
||||
fn create_asset_id(seed: u32) -> AssetId;
|
||||
/// Setup assets for benchmarking (create and mint)
|
||||
fn setup_assets(
|
||||
payment_asset: AssetId,
|
||||
reward_asset: AssetId,
|
||||
admin: &AccountId,
|
||||
accounts: &[AccountId],
|
||||
payment_amount: u128,
|
||||
reward_amount: u128,
|
||||
);
|
||||
}
|
||||
|
||||
impl<AssetId: From<u32>, AccountId> BenchmarkHelper<AssetId, AccountId> for () {
|
||||
fn create_asset_id(seed: u32) -> AssetId {
|
||||
seed.into()
|
||||
}
|
||||
fn setup_assets(
|
||||
_payment_asset: AssetId,
|
||||
_reward_asset: AssetId,
|
||||
_admin: &AccountId,
|
||||
_accounts: &[AccountId],
|
||||
_payment_amount: u128,
|
||||
_reward_amount: u128,
|
||||
) {
|
||||
// Default implementation does nothing
|
||||
// Runtime should provide actual implementation
|
||||
}
|
||||
}
|
||||
|
||||
#[benchmarks(
|
||||
where
|
||||
T::AssetId: From<u32>,
|
||||
T::Assets: Create<T::AccountId> + Mutate<T::AccountId>,
|
||||
)]
|
||||
mod benchmarks {
|
||||
use super::*;
|
||||
use pezframe_support::traits::{fungibles::Create, Get};
|
||||
|
||||
fn get_asset_id<T: Config>(seed: u32) -> T::AssetId
|
||||
where
|
||||
T::AssetId: From<u32>,
|
||||
{
|
||||
seed.into()
|
||||
}
|
||||
|
||||
/// Setup assets for presale benchmarking
|
||||
/// Creates payment and reward assets, mints to necessary accounts
|
||||
fn setup_benchmark_assets<T: Config>(
|
||||
caller: &T::AccountId,
|
||||
presale_treasury: &T::AccountId,
|
||||
) -> (T::AssetId, T::AssetId)
|
||||
where
|
||||
T::AssetId: From<u32>,
|
||||
T::Assets: Create<T::AccountId> + Mutate<T::AccountId>,
|
||||
{
|
||||
let payment_asset = get_asset_id::<T>(1);
|
||||
let reward_asset = get_asset_id::<T>(2);
|
||||
|
||||
// Create assets if they don't exist (ignore errors if already created)
|
||||
let min_balance: T::Balance = 1u128.into();
|
||||
let _ = T::Assets::create(payment_asset.clone(), caller.clone(), true, min_balance);
|
||||
let _ = T::Assets::create(reward_asset.clone(), caller.clone(), true, min_balance);
|
||||
|
||||
// Mint payment tokens to caller for contributions
|
||||
let payment_amount: T::Balance = 100_000_000u128.into();
|
||||
let _ = T::Assets::mint_into(payment_asset.clone(), caller, payment_amount);
|
||||
|
||||
// Mint payment tokens to platform accounts for fee distribution
|
||||
let _ = T::Assets::mint_into(
|
||||
payment_asset.clone(),
|
||||
&T::PlatformTreasury::get(),
|
||||
payment_amount,
|
||||
);
|
||||
let _ = T::Assets::mint_into(
|
||||
payment_asset.clone(),
|
||||
&T::StakingRewardPool::get(),
|
||||
payment_amount,
|
||||
);
|
||||
|
||||
// Mint reward tokens to presale treasury for distribution
|
||||
let reward_amount: T::Balance = 10_000_000_000u128.into();
|
||||
let _ = T::Assets::mint_into(reward_asset.clone(), presale_treasury, reward_amount);
|
||||
|
||||
(payment_asset, reward_asset)
|
||||
}
|
||||
|
||||
/// Create a presale with standard parameters
|
||||
fn create_test_presale<T: Config>(
|
||||
caller: &T::AccountId,
|
||||
payment_asset: T::AssetId,
|
||||
reward_asset: T::AssetId,
|
||||
is_whitelist: bool,
|
||||
enable_vesting: bool,
|
||||
) -> PresaleId
|
||||
where
|
||||
T::AssetId: From<u32>,
|
||||
{
|
||||
let presale_id = NextPresaleId::<T>::get();
|
||||
|
||||
let _ = Presale::<T>::create_presale(
|
||||
RawOrigin::Signed(caller.clone()).into(),
|
||||
payment_asset,
|
||||
reward_asset,
|
||||
10_000_000_000u128, // tokens_for_sale (10M)
|
||||
1000u32.into(), // duration (long enough for tests)
|
||||
is_whitelist,
|
||||
100u128, // min_contribution
|
||||
10_000_000u128, // max_contribution
|
||||
1_000_000u128, // soft_cap
|
||||
100_000_000u128, // hard_cap
|
||||
enable_vesting,
|
||||
if enable_vesting { 20u8 } else { 0u8 }, // 20% immediate if vesting
|
||||
if enable_vesting { 100u32.into() } else { 0u32.into() }, // vesting_duration
|
||||
if enable_vesting { 10u32.into() } else { 0u32.into() }, // cliff
|
||||
10u32.into(), // grace_period_blocks
|
||||
5u8, // refund_fee_percent
|
||||
2u8, // grace_refund_fee_percent
|
||||
);
|
||||
|
||||
presale_id
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn create_presale() {
|
||||
let caller: T::AccountId = whitelisted_caller();
|
||||
let payment_asset = get_asset_id::<T>(1);
|
||||
let reward_asset = get_asset_id::<T>(2);
|
||||
|
||||
#[extrinsic_call]
|
||||
create_presale(
|
||||
RawOrigin::Signed(caller),
|
||||
payment_asset,
|
||||
reward_asset,
|
||||
1_000_000u128, // tokens_for_sale
|
||||
100u32.into(), // duration
|
||||
false, // is_whitelist
|
||||
100u128, // min_contribution
|
||||
10_000u128, // max_contribution
|
||||
500_000u128, // soft_cap
|
||||
1_000_000u128, // hard_cap
|
||||
false, // enable_vesting
|
||||
0u8, // vesting_immediate_percent
|
||||
0u32.into(), // vesting_duration_blocks
|
||||
0u32.into(), // vesting_cliff_blocks
|
||||
10u32.into(), // grace_period_blocks
|
||||
5u8, // refund_fee_percent
|
||||
10u8, // grace_refund_fee_percent
|
||||
);
|
||||
|
||||
// Verify presale was created
|
||||
assert!(crate::Presales::<T>::contains_key(0));
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn cancel_presale() {
|
||||
let caller: T::AccountId = whitelisted_caller();
|
||||
let payment_asset = get_asset_id::<T>(1);
|
||||
let reward_asset = get_asset_id::<T>(2);
|
||||
|
||||
// Create a presale first
|
||||
let presale_id =
|
||||
create_test_presale::<T>(&caller, payment_asset, reward_asset, false, false);
|
||||
|
||||
#[extrinsic_call]
|
||||
cancel_presale(RawOrigin::Root, presale_id);
|
||||
|
||||
// Verify presale was cancelled
|
||||
let presale = crate::Presales::<T>::get(presale_id).unwrap();
|
||||
assert_eq!(presale.status, PresaleStatus::Cancelled);
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn add_to_whitelist() {
|
||||
let owner: T::AccountId = whitelisted_caller();
|
||||
let user: T::AccountId = account("user", 0, 0);
|
||||
let payment_asset = get_asset_id::<T>(1);
|
||||
let reward_asset = get_asset_id::<T>(2);
|
||||
|
||||
// Create a whitelist presale
|
||||
let presale_id = create_test_presale::<T>(&owner, payment_asset, reward_asset, true, false);
|
||||
|
||||
#[extrinsic_call]
|
||||
add_to_whitelist(RawOrigin::Signed(owner), presale_id, user.clone());
|
||||
|
||||
// Verify user was whitelisted
|
||||
assert!(crate::WhitelistedAccounts::<T>::get(presale_id, &user));
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn contribute() {
|
||||
let caller: T::AccountId = whitelisted_caller();
|
||||
// Get next presale ID before creating
|
||||
let presale_id = NextPresaleId::<T>::get();
|
||||
let presale_treasury = Presale::<T>::presale_account_id(presale_id);
|
||||
|
||||
// Setup assets
|
||||
let (payment_asset, reward_asset) = setup_benchmark_assets::<T>(&caller, &presale_treasury);
|
||||
|
||||
// Create presale (will get the presale_id we calculated)
|
||||
let _ = create_test_presale::<T>(&caller, payment_asset, reward_asset, false, false);
|
||||
|
||||
let amount: u128 = 10_000u128;
|
||||
|
||||
#[extrinsic_call]
|
||||
contribute(RawOrigin::Signed(caller.clone()), presale_id, amount);
|
||||
|
||||
// Verify contribution was recorded
|
||||
assert!(crate::Contributions::<T>::get(presale_id, &caller).is_some());
|
||||
assert!(crate::TotalRaised::<T>::get(presale_id) > 0);
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn refund() {
|
||||
let caller: T::AccountId = whitelisted_caller();
|
||||
// Get next presale ID before creating
|
||||
let presale_id = NextPresaleId::<T>::get();
|
||||
let presale_treasury = Presale::<T>::presale_account_id(presale_id);
|
||||
|
||||
// Setup assets
|
||||
let (payment_asset, reward_asset) = setup_benchmark_assets::<T>(&caller, &presale_treasury);
|
||||
|
||||
// Create presale (will get the presale_id we calculated)
|
||||
let _ = create_test_presale::<T>(&caller, payment_asset, reward_asset, false, false);
|
||||
|
||||
// Make a contribution first
|
||||
let amount: u128 = 10_000u128;
|
||||
let _ =
|
||||
Presale::<T>::contribute(RawOrigin::Signed(caller.clone()).into(), presale_id, amount);
|
||||
|
||||
// Verify contribution exists
|
||||
assert!(crate::Contributions::<T>::get(presale_id, &caller).is_some());
|
||||
|
||||
#[extrinsic_call]
|
||||
refund(RawOrigin::Signed(caller.clone()), presale_id);
|
||||
|
||||
// Verify refund was processed
|
||||
let contribution = crate::Contributions::<T>::get(presale_id, &caller).unwrap();
|
||||
assert!(contribution.refunded);
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn claim_vested() {
|
||||
let caller: T::AccountId = whitelisted_caller();
|
||||
// Get next presale ID before creating
|
||||
let presale_id = NextPresaleId::<T>::get();
|
||||
let presale_treasury = Presale::<T>::presale_account_id(presale_id);
|
||||
|
||||
// Setup assets
|
||||
let (payment_asset, reward_asset) = setup_benchmark_assets::<T>(&caller, &presale_treasury);
|
||||
|
||||
// Mint EXTRA reward tokens to presale treasury to prevent account death
|
||||
let extra_reward: T::Balance = 100_000_000_000u128.into();
|
||||
let _ = T::Assets::mint_into(reward_asset.clone(), &presale_treasury, extra_reward);
|
||||
|
||||
// Create presale WITH vesting (will get the presale_id we calculated)
|
||||
let _ = create_test_presale::<T>(&caller, payment_asset, reward_asset, false, true);
|
||||
|
||||
// Make a contribution
|
||||
let amount: u128 = 1_000_000u128; // Large enough to reach soft cap
|
||||
let _ =
|
||||
Presale::<T>::contribute(RawOrigin::Signed(caller.clone()).into(), presale_id, amount);
|
||||
|
||||
// Advance blocks past presale end
|
||||
pezframe_system::Pallet::<T>::set_block_number(2000u32.into());
|
||||
|
||||
// Finalize presale (requires root)
|
||||
let _ = Presale::<T>::finalize_presale(RawOrigin::Root.into(), presale_id);
|
||||
|
||||
// Advance past cliff period
|
||||
pezframe_system::Pallet::<T>::set_block_number(3000u32.into());
|
||||
|
||||
#[extrinsic_call]
|
||||
claim_vested(RawOrigin::Signed(caller.clone()), presale_id);
|
||||
|
||||
// Verify claim was recorded
|
||||
let claimed = crate::VestingClaimed::<T>::get(presale_id, &caller);
|
||||
assert!(claimed > 0);
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn refund_cancelled_presale() {
|
||||
let caller: T::AccountId = whitelisted_caller();
|
||||
// Get next presale ID before creating
|
||||
let presale_id = NextPresaleId::<T>::get();
|
||||
let presale_treasury = Presale::<T>::presale_account_id(presale_id);
|
||||
|
||||
// Setup assets
|
||||
let (payment_asset, reward_asset) = setup_benchmark_assets::<T>(&caller, &presale_treasury);
|
||||
|
||||
// Create presale (will get the presale_id we calculated)
|
||||
let _ =
|
||||
create_test_presale::<T>(&caller, payment_asset.clone(), reward_asset, false, false);
|
||||
|
||||
// Make a contribution
|
||||
let amount: u128 = 10_000u128;
|
||||
let _ =
|
||||
Presale::<T>::contribute(RawOrigin::Signed(caller.clone()).into(), presale_id, amount);
|
||||
|
||||
// Mint payment tokens to presale treasury for refund
|
||||
let refund_amount: T::Balance = 100_000u128.into();
|
||||
let _ = T::Assets::mint_into(payment_asset, &presale_treasury, refund_amount);
|
||||
|
||||
// Cancel the presale
|
||||
let _ = Presale::<T>::cancel_presale(RawOrigin::Root.into(), presale_id);
|
||||
|
||||
#[extrinsic_call]
|
||||
refund_cancelled_presale(RawOrigin::Signed(caller.clone()), presale_id);
|
||||
|
||||
// Verify refund was processed
|
||||
let contribution = crate::Contributions::<T>::get(presale_id, &caller).unwrap();
|
||||
assert!(contribution.refunded);
|
||||
}
|
||||
|
||||
/// Benchmark finalize_presale with variable number of contributors
|
||||
/// This is O(N) complexity - critical for proper weight calculation
|
||||
#[benchmark]
|
||||
fn finalize_presale(n: Linear<1, 100>) {
|
||||
let caller: T::AccountId = whitelisted_caller();
|
||||
// Get next presale ID before creating
|
||||
let presale_id = NextPresaleId::<T>::get();
|
||||
let presale_treasury = Presale::<T>::presale_account_id(presale_id);
|
||||
|
||||
// Setup assets with enough for many contributors
|
||||
let (payment_asset, reward_asset) = setup_benchmark_assets::<T>(&caller, &presale_treasury);
|
||||
|
||||
// Create presale (will get the presale_id we calculated)
|
||||
let _ = create_test_presale::<T>(
|
||||
&caller,
|
||||
payment_asset.clone(),
|
||||
reward_asset.clone(),
|
||||
false,
|
||||
false,
|
||||
);
|
||||
|
||||
// Add n contributors
|
||||
for i in 0..n {
|
||||
let contributor: T::AccountId = account("contributor", i, 0);
|
||||
|
||||
// Mint payment tokens to contributor
|
||||
let contribution_amount: T::Balance = 50_000u128.into();
|
||||
let _ = T::Assets::mint_into(payment_asset.clone(), &contributor, contribution_amount);
|
||||
|
||||
// Make contribution
|
||||
let _ = Presale::<T>::contribute(
|
||||
RawOrigin::Signed(contributor).into(),
|
||||
presale_id,
|
||||
10_000u128,
|
||||
);
|
||||
}
|
||||
|
||||
// Advance blocks past presale end
|
||||
pezframe_system::Pallet::<T>::set_block_number(2000u32.into());
|
||||
|
||||
#[extrinsic_call]
|
||||
finalize_presale(RawOrigin::Root, presale_id);
|
||||
|
||||
// Verify presale was finalized
|
||||
let presale = crate::Presales::<T>::get(presale_id).unwrap();
|
||||
assert!(
|
||||
presale.status == PresaleStatus::Finalized || presale.status == PresaleStatus::Failed
|
||||
);
|
||||
}
|
||||
|
||||
/// Benchmark batch_refund_failed_presale with variable batch size
|
||||
/// This is also O(N) complexity
|
||||
#[benchmark]
|
||||
fn batch_refund_failed_presale(n: Linear<1, 100>) {
|
||||
let caller: T::AccountId = whitelisted_caller();
|
||||
// Get next presale ID before creating
|
||||
let presale_id = NextPresaleId::<T>::get();
|
||||
let presale_treasury = Presale::<T>::presale_account_id(presale_id);
|
||||
|
||||
// Setup assets
|
||||
let (payment_asset, reward_asset) = setup_benchmark_assets::<T>(&caller, &presale_treasury);
|
||||
|
||||
// Create presale with HIGH soft cap (will fail)
|
||||
let _ = Presale::<T>::create_presale(
|
||||
RawOrigin::Signed(caller.clone()).into(),
|
||||
payment_asset.clone(),
|
||||
reward_asset,
|
||||
10_000_000_000u128, // tokens_for_sale
|
||||
1000u32.into(), // duration
|
||||
false,
|
||||
100u128, // min_contribution
|
||||
10_000_000u128, // max_contribution
|
||||
1_000_000_000_000u128, // soft_cap (very high - will fail)
|
||||
2_000_000_000_000u128, // hard_cap
|
||||
false,
|
||||
0u8,
|
||||
0u32.into(),
|
||||
0u32.into(),
|
||||
10u32.into(),
|
||||
5u8,
|
||||
2u8,
|
||||
);
|
||||
|
||||
// Add n contributors (small amounts that won't reach soft cap)
|
||||
for i in 0..n {
|
||||
let contributor: T::AccountId = account("contributor", i, 0);
|
||||
|
||||
// Mint payment tokens to contributor
|
||||
let contribution_amount: T::Balance = 50_000u128.into();
|
||||
let _ = T::Assets::mint_into(payment_asset.clone(), &contributor, contribution_amount);
|
||||
|
||||
// Make small contribution
|
||||
let _ = Presale::<T>::contribute(
|
||||
RawOrigin::Signed(contributor).into(),
|
||||
presale_id,
|
||||
1_000u128,
|
||||
);
|
||||
}
|
||||
|
||||
// Mint payment tokens to presale treasury for refunds
|
||||
let refund_pool: T::Balance = (n as u128 * 10_000u128).into();
|
||||
let _ = T::Assets::mint_into(payment_asset.clone(), &presale_treasury, refund_pool);
|
||||
|
||||
// Advance blocks past presale end
|
||||
pezframe_system::Pallet::<T>::set_block_number(2000u32.into());
|
||||
|
||||
// Finalize presale (will mark as Failed due to soft cap not reached)
|
||||
let _ = Presale::<T>::finalize_presale(RawOrigin::Root.into(), presale_id);
|
||||
|
||||
// Verify presale failed
|
||||
let presale = crate::Presales::<T>::get(presale_id).unwrap();
|
||||
assert_eq!(presale.status, PresaleStatus::Failed);
|
||||
|
||||
#[extrinsic_call]
|
||||
batch_refund_failed_presale(RawOrigin::Signed(caller), presale_id, 0, n);
|
||||
|
||||
// Verify refunds were processed
|
||||
let first_contributor: T::AccountId = account("contributor", 0, 0);
|
||||
let contribution = crate::Contributions::<T>::get(presale_id, &first_contributor);
|
||||
if let Some(c) = contribution {
|
||||
assert!(c.refunded);
|
||||
}
|
||||
}
|
||||
|
||||
impl_benchmark_test_suite!(Presale, crate::mock::new_test_ext(), crate::mock::Test);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,195 @@
|
||||
use crate as pezpallet_presale;
|
||||
use pezframe_support::{
|
||||
parameter_types,
|
||||
traits::{ConstU128, ConstU16, ConstU32, ConstU64},
|
||||
PalletId,
|
||||
};
|
||||
use pezsp_core::H256;
|
||||
use pezsp_runtime::{
|
||||
traits::{BlakeTwo256, IdentityLookup},
|
||||
BuildStorage,
|
||||
};
|
||||
|
||||
type Block = pezframe_system::mocking::MockBlock<Test>;
|
||||
|
||||
// Configure a mock runtime to test the pallet.
|
||||
pezframe_support::construct_runtime!(
|
||||
pub enum Test
|
||||
{
|
||||
System: pezframe_system,
|
||||
Balances: pezpallet_balances,
|
||||
Assets: pezpallet_assets,
|
||||
Presale: pezpallet_presale,
|
||||
}
|
||||
);
|
||||
|
||||
impl pezframe_system::Config for Test {
|
||||
type BaseCallFilter = pezframe_support::traits::Everything;
|
||||
type BlockWeights = ();
|
||||
type BlockLength = ();
|
||||
type DbWeight = ();
|
||||
type RuntimeOrigin = RuntimeOrigin;
|
||||
type RuntimeCall = RuntimeCall;
|
||||
type Nonce = u64;
|
||||
type Hash = H256;
|
||||
type Hashing = BlakeTwo256;
|
||||
type AccountId = u64;
|
||||
type Lookup = IdentityLookup<Self::AccountId>;
|
||||
type Block = Block;
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type BlockHashCount = ConstU64<250>;
|
||||
type Version = ();
|
||||
type PalletInfo = PalletInfo;
|
||||
type AccountData = pezpallet_balances::AccountData<u128>;
|
||||
type OnNewAccount = ();
|
||||
type OnKilledAccount = ();
|
||||
type SystemWeightInfo = ();
|
||||
type SS58Prefix = ConstU16<42>;
|
||||
type OnSetCode = ();
|
||||
type MaxConsumers = ConstU32<16>;
|
||||
type RuntimeTask = ();
|
||||
type ExtensionsWeightInfo = ();
|
||||
type SingleBlockMigrations = ();
|
||||
type MultiBlockMigrator = ();
|
||||
type PreInherents = ();
|
||||
type PostInherents = ();
|
||||
type PostTransactions = ();
|
||||
}
|
||||
|
||||
impl pezpallet_balances::Config for Test {
|
||||
type MaxLocks = ();
|
||||
type MaxReserves = ();
|
||||
type ReserveIdentifier = [u8; 8];
|
||||
type Balance = u128;
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type DustRemoval = ();
|
||||
type ExistentialDeposit = ConstU128<1>;
|
||||
type AccountStore = System;
|
||||
type WeightInfo = ();
|
||||
type FreezeIdentifier = ();
|
||||
type MaxFreezes = ();
|
||||
type RuntimeHoldReason = ();
|
||||
type RuntimeFreezeReason = ();
|
||||
type DoneSlashHandler = ();
|
||||
}
|
||||
|
||||
impl pezpallet_assets::Config for Test {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type Balance = u128;
|
||||
type AssetId = u32;
|
||||
type AssetIdParameter = u32;
|
||||
type Currency = Balances;
|
||||
type CreateOrigin =
|
||||
pezframe_support::traits::AsEnsureOriginWithArg<pezframe_system::EnsureSigned<u64>>;
|
||||
type ForceOrigin = pezframe_system::EnsureRoot<u64>;
|
||||
type AssetDeposit = ConstU128<1>;
|
||||
type AssetAccountDeposit = ConstU128<0>; // No deposit required for test environment
|
||||
type MetadataDepositBase = ConstU128<1>;
|
||||
type MetadataDepositPerByte = ConstU128<1>;
|
||||
type ApprovalDeposit = ConstU128<1>;
|
||||
type StringLimit = ConstU32<50>;
|
||||
type Freezer = ();
|
||||
type Extra = ();
|
||||
type WeightInfo = ();
|
||||
type RemoveItemsLimit = ConstU32<1000>;
|
||||
type CallbackHandle = ();
|
||||
type Holder = ();
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
type BenchmarkHelper = ();
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const PresalePalletId: PalletId = PalletId(*b"py/prsal");
|
||||
pub const PlatformFeePercent: u8 = 2;
|
||||
pub const MaxContributors: u32 = 10000;
|
||||
pub const MaxBonusTiers: u32 = 5;
|
||||
pub const MaxWhitelistedAccounts: u32 = 10000;
|
||||
pub PlatformTreasuryAccount: u64 = 999;
|
||||
pub StakingRewardPoolAccount: u64 = 998;
|
||||
}
|
||||
|
||||
impl pezpallet_presale::Config for Test {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type AssetId = u32;
|
||||
type Balance = u128;
|
||||
type Assets = Assets;
|
||||
type PalletId = PresalePalletId;
|
||||
type PlatformTreasury = PlatformTreasuryAccount;
|
||||
type StakingRewardPool = StakingRewardPoolAccount;
|
||||
type PlatformFeePercent = PlatformFeePercent;
|
||||
type MaxContributors = MaxContributors;
|
||||
type MaxBonusTiers = MaxBonusTiers;
|
||||
type MaxWhitelistedAccounts = MaxWhitelistedAccounts;
|
||||
type CreatePresaleOrigin = pezframe_system::EnsureSigned<u64>;
|
||||
type EmergencyOrigin = pezframe_system::EnsureRoot<u64>;
|
||||
type PresaleWeightInfo = crate::weights::BizinikiwiWeight<Test>;
|
||||
}
|
||||
|
||||
// Build genesis storage according to the mock runtime.
|
||||
pub fn new_test_ext() -> pezsp_io::TestExternalities {
|
||||
let mut t = pezframe_system::GenesisConfig::<Test>::default().build_storage().unwrap();
|
||||
|
||||
pezpallet_balances::GenesisConfig::<Test> {
|
||||
balances: vec![
|
||||
(1, 1_000_000_000_000_000), // Alice
|
||||
(2, 1_000_000_000_000_000), // Bob
|
||||
(3, 1_000_000_000_000_000), // Charlie
|
||||
(999, 1_000_000_000_000_000), // Platform Treasury
|
||||
(998, 1_000_000_000_000_000), // Staking Pool
|
||||
],
|
||||
dev_accounts: None,
|
||||
}
|
||||
.assimilate_storage(&mut t)
|
||||
.unwrap();
|
||||
|
||||
let mut ext = pezsp_io::TestExternalities::new(t);
|
||||
ext.execute_with(|| System::set_block_number(1));
|
||||
ext
|
||||
}
|
||||
|
||||
// Helper to create assets
|
||||
pub fn create_assets() {
|
||||
use pezframe_support::assert_ok;
|
||||
|
||||
// Create PEZ asset (ID: 1)
|
||||
assert_ok!(Assets::force_create(
|
||||
RuntimeOrigin::root(),
|
||||
1u32,
|
||||
1, // Alice as admin
|
||||
true,
|
||||
1
|
||||
));
|
||||
|
||||
// Create wUSDT asset (ID: 2)
|
||||
assert_ok!(Assets::force_create(
|
||||
RuntimeOrigin::root(),
|
||||
2u32,
|
||||
1, // Alice as admin
|
||||
true,
|
||||
1
|
||||
));
|
||||
}
|
||||
|
||||
// Helper to mint assets to accounts
|
||||
pub fn mint_assets(asset_id: u32, account: u64, amount: u128) {
|
||||
use pezframe_support::assert_ok;
|
||||
|
||||
assert_ok!(Assets::mint(RuntimeOrigin::signed(1), asset_id.into(), account, amount));
|
||||
}
|
||||
|
||||
// Helper to get presale sub-account treasury for a specific presale ID
|
||||
pub fn presale_treasury(presale_id: u32) -> u64 {
|
||||
use pezsp_io::hashing::blake2_256;
|
||||
|
||||
// Create a unique account ID for each presale by hashing pezpallet_id + presale_id
|
||||
// This matches the logic in pezpallet_presale::Pallet::presale_account_id
|
||||
let pezpallet_id = PresalePalletId::get();
|
||||
let mut buf = Vec::new();
|
||||
buf.extend_from_slice(&pezpallet_id.0[..]);
|
||||
buf.extend_from_slice(&presale_id.to_le_bytes());
|
||||
let hash = blake2_256(&buf);
|
||||
|
||||
// Convert hash to u64 (since Test uses u64 as AccountId)
|
||||
// Take first 8 bytes and convert to u64
|
||||
u64::from_le_bytes([hash[0], hash[1], hash[2], hash[3], hash[4], hash[5], hash[6], hash[7]])
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,466 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
|
||||
//! Autogenerated weights for `pezpallet_presale`
|
||||
//!
|
||||
//! THIS FILE WAS AUTO-GENERATED USING THE BIZINIKIWI BENCHMARK CLI VERSION 32.0.0
|
||||
//! DATE: 2025-12-08, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
|
||||
//! WORST CASE MAP SIZE: `1000000`
|
||||
//! HOSTNAME: `MamostePC`, CPU: `11th Gen Intel(R) Core(TM) i9-11950H @ 2.60GHz`
|
||||
//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: `1024`
|
||||
|
||||
// Executed Command:
|
||||
// ./target/release/frame-omni-bencher
|
||||
// v1
|
||||
// benchmark
|
||||
// pallet
|
||||
// --runtime
|
||||
// target/release/wbuild/asset-hub-pezkuwichain-runtime/asset_hub_pezkuwichain_runtime.compact.compressed.wasm
|
||||
// --pallets
|
||||
// pezpallet_presale
|
||||
// -e
|
||||
// all
|
||||
// --steps
|
||||
// 50
|
||||
// --repeat
|
||||
// 20
|
||||
// --output
|
||||
// pezcumulus/teyrchains/pallets/presale/src/weights.rs
|
||||
// --template
|
||||
// bizinikiwi/.maintain/frame-weight-template.hbs
|
||||
|
||||
#![cfg_attr(rustfmt, rustfmt_skip)]
|
||||
#![allow(unused_parens)]
|
||||
#![allow(unused_imports)]
|
||||
#![allow(missing_docs)]
|
||||
#![allow(dead_code)]
|
||||
|
||||
use pezframe_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}};
|
||||
use core::marker::PhantomData;
|
||||
|
||||
/// Weight functions needed for `pezpallet_presale`.
|
||||
pub trait WeightInfo {
|
||||
fn create_presale() -> Weight;
|
||||
fn cancel_presale() -> Weight;
|
||||
fn add_to_whitelist() -> Weight;
|
||||
fn contribute() -> Weight;
|
||||
fn refund() -> Weight;
|
||||
fn claim_vested() -> Weight;
|
||||
fn refund_cancelled_presale() -> Weight;
|
||||
fn finalize_presale(n: u32, ) -> Weight;
|
||||
fn batch_refund_failed_presale(n: u32, ) -> Weight;
|
||||
}
|
||||
|
||||
/// Weights for `pezpallet_presale` using the Bizinikiwi node and recommended hardware.
|
||||
pub struct BizinikiwiWeight<T>(PhantomData<T>);
|
||||
impl<T: pezframe_system::Config> WeightInfo for BizinikiwiWeight<T> {
|
||||
/// Storage: `Presale::NextPresaleId` (r:1 w:1)
|
||||
/// Proof: `Presale::NextPresaleId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Presale::Presales` (r:0 w:1)
|
||||
/// Proof: `Presale::Presales` (`max_values`: None, `max_size`: Some(252), added: 2727, mode: `MaxEncodedLen`)
|
||||
fn create_presale() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `147`
|
||||
// Estimated: `1489`
|
||||
// Minimum execution time: 9_638_000 picoseconds.
|
||||
Weight::from_parts(10_003_000, 1489)
|
||||
.saturating_add(T::DbWeight::get().reads(1_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(2_u64))
|
||||
}
|
||||
/// Storage: `Presale::Presales` (r:1 w:1)
|
||||
/// Proof: `Presale::Presales` (`max_values`: None, `max_size`: Some(252), added: 2727, mode: `MaxEncodedLen`)
|
||||
fn cancel_presale() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `356`
|
||||
// Estimated: `3717`
|
||||
// Minimum execution time: 12_202_000 picoseconds.
|
||||
Weight::from_parts(12_492_000, 3717)
|
||||
.saturating_add(T::DbWeight::get().reads(1_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(1_u64))
|
||||
}
|
||||
/// Storage: `Presale::Presales` (r:1 w:0)
|
||||
/// Proof: `Presale::Presales` (`max_values`: None, `max_size`: Some(252), added: 2727, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Presale::WhitelistedAccounts` (r:0 w:1)
|
||||
/// Proof: `Presale::WhitelistedAccounts` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`)
|
||||
fn add_to_whitelist() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `356`
|
||||
// Estimated: `3717`
|
||||
// Minimum execution time: 12_973_000 picoseconds.
|
||||
Weight::from_parts(14_077_000, 3717)
|
||||
.saturating_add(T::DbWeight::get().reads(1_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(1_u64))
|
||||
}
|
||||
/// Storage: `Presale::Presales` (r:1 w:0)
|
||||
/// Proof: `Presale::Presales` (`max_values`: None, `max_size`: Some(252), added: 2727, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Presale::Contributions` (r:1 w:1)
|
||||
/// Proof: `Presale::Contributions` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Presale::TotalRaised` (r:1 w:1)
|
||||
/// Proof: `Presale::TotalRaised` (`max_values`: None, `max_size`: Some(36), added: 2511, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Assets::Asset` (r:1 w:1)
|
||||
/// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Assets::Account` (r:4 w:4)
|
||||
/// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
|
||||
/// Storage: `AssetsFreezer::FrozenBalances` (r:1 w:0)
|
||||
/// Proof: `AssetsFreezer::FrozenBalances` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
|
||||
/// Storage: `System::Account` (r:1 w:1)
|
||||
/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Presale::Contributors` (r:1 w:1)
|
||||
/// Proof: `Presale::Contributors` (`max_values`: None, `max_size`: Some(320022), added: 322497, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Presale::TotalPlatformVolume` (r:1 w:1)
|
||||
/// Proof: `Presale::TotalPlatformVolume` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Presale::TotalPlatformFees` (r:1 w:1)
|
||||
/// Proof: `Presale::TotalPlatformFees` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`)
|
||||
fn contribute() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `1169`
|
||||
// Estimated: `323487`
|
||||
// Minimum execution time: 160_665_000 picoseconds.
|
||||
Weight::from_parts(165_629_000, 323487)
|
||||
.saturating_add(T::DbWeight::get().reads(13_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(11_u64))
|
||||
}
|
||||
/// Storage: `Presale::Presales` (r:1 w:0)
|
||||
/// Proof: `Presale::Presales` (`max_values`: None, `max_size`: Some(252), added: 2727, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Presale::Contributions` (r:1 w:1)
|
||||
/// Proof: `Presale::Contributions` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Assets::Asset` (r:1 w:1)
|
||||
/// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Assets::Account` (r:4 w:4)
|
||||
/// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
|
||||
/// Storage: `AssetsFreezer::FrozenBalances` (r:1 w:1)
|
||||
/// Proof: `AssetsFreezer::FrozenBalances` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
|
||||
/// Storage: `System::Account` (r:1 w:1)
|
||||
/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
|
||||
/// Storage: `AssetsFreezer::Freezes` (r:1 w:1)
|
||||
/// Proof: `AssetsFreezer::Freezes` (`max_values`: None, `max_size`: Some(105), added: 2580, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Presale::TotalRaised` (r:1 w:1)
|
||||
/// Proof: `Presale::TotalRaised` (`max_values`: None, `max_size`: Some(36), added: 2511, mode: `MaxEncodedLen`)
|
||||
fn refund() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `1531`
|
||||
// Estimated: `11426`
|
||||
// Minimum execution time: 169_420_000 picoseconds.
|
||||
Weight::from_parts(173_774_000, 11426)
|
||||
.saturating_add(T::DbWeight::get().reads(11_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(10_u64))
|
||||
}
|
||||
/// Storage: `Presale::Presales` (r:1 w:0)
|
||||
/// Proof: `Presale::Presales` (`max_values`: None, `max_size`: Some(252), added: 2727, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Presale::Contributions` (r:1 w:0)
|
||||
/// Proof: `Presale::Contributions` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Presale::TotalRaised` (r:1 w:0)
|
||||
/// Proof: `Presale::TotalRaised` (`max_values`: None, `max_size`: Some(36), added: 2511, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Presale::VestingClaimed` (r:1 w:1)
|
||||
/// Proof: `Presale::VestingClaimed` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Assets::Asset` (r:1 w:1)
|
||||
/// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Assets::Account` (r:2 w:2)
|
||||
/// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
|
||||
/// Storage: `AssetsFreezer::FrozenBalances` (r:1 w:0)
|
||||
/// Proof: `AssetsFreezer::FrozenBalances` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
|
||||
fn claim_vested() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `1392`
|
||||
// Estimated: `6208`
|
||||
// Minimum execution time: 72_526_000 picoseconds.
|
||||
Weight::from_parts(74_529_000, 6208)
|
||||
.saturating_add(T::DbWeight::get().reads(8_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(4_u64))
|
||||
}
|
||||
/// Storage: `Presale::Presales` (r:1 w:0)
|
||||
/// Proof: `Presale::Presales` (`max_values`: None, `max_size`: Some(252), added: 2727, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Presale::Contributors` (r:1 w:0)
|
||||
/// Proof: `Presale::Contributors` (`max_values`: None, `max_size`: Some(320022), added: 322497, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Presale::Contributions` (r:1 w:1)
|
||||
/// Proof: `Presale::Contributions` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Assets::Asset` (r:1 w:1)
|
||||
/// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Assets::Account` (r:2 w:2)
|
||||
/// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
|
||||
/// Storage: `AssetsFreezer::FrozenBalances` (r:1 w:0)
|
||||
/// Proof: `AssetsFreezer::FrozenBalances` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
|
||||
fn refund_cancelled_presale() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `1370`
|
||||
// Estimated: `323487`
|
||||
// Minimum execution time: 66_454_000 picoseconds.
|
||||
Weight::from_parts(69_615_000, 323487)
|
||||
.saturating_add(T::DbWeight::get().reads(7_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(4_u64))
|
||||
}
|
||||
/// Storage: `Presale::Presales` (r:1 w:1)
|
||||
/// Proof: `Presale::Presales` (`max_values`: None, `max_size`: Some(252), added: 2727, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Presale::TotalRaised` (r:1 w:0)
|
||||
/// Proof: `Presale::TotalRaised` (`max_values`: None, `max_size`: Some(36), added: 2511, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Presale::Contributors` (r:1 w:0)
|
||||
/// Proof: `Presale::Contributors` (`max_values`: None, `max_size`: Some(320022), added: 322497, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Presale::Contributions` (r:100 w:0)
|
||||
/// Proof: `Presale::Contributions` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Assets::Asset` (r:1 w:1)
|
||||
/// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Assets::Account` (r:101 w:101)
|
||||
/// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
|
||||
/// Storage: `AssetsFreezer::FrozenBalances` (r:1 w:1)
|
||||
/// Proof: `AssetsFreezer::FrozenBalances` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
|
||||
/// Storage: `System::Account` (r:101 w:101)
|
||||
/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
|
||||
/// Storage: `AssetsFreezer::Freezes` (r:1 w:1)
|
||||
/// Proof: `AssetsFreezer::Freezes` (`max_values`: None, `max_size`: Some(105), added: 2580, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Presale::SuccessfulPresales` (r:1 w:1)
|
||||
/// Proof: `Presale::SuccessfulPresales` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
|
||||
/// The range of component `n` is `[1, 100]`.
|
||||
fn finalize_presale(n: u32, ) -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `525`
|
||||
// Estimated: `3717 + n * (3235 ±0)`
|
||||
// Minimum execution time: 17_663_000 picoseconds.
|
||||
Weight::from_parts(18_454_000, 3717)
|
||||
// Standard Error: 463_975
|
||||
.saturating_add(Weight::from_parts(2_728_899, 0).saturating_mul(n.into()))
|
||||
.saturating_add(T::DbWeight::get().reads(2_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(1_u64))
|
||||
.saturating_add(Weight::from_parts(0, 3235).saturating_mul(n.into()))
|
||||
}
|
||||
/// Storage: `Presale::Presales` (r:1 w:0)
|
||||
/// Proof: `Presale::Presales` (`max_values`: None, `max_size`: Some(252), added: 2727, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Presale::Contributors` (r:1 w:0)
|
||||
/// Proof: `Presale::Contributors` (`max_values`: None, `max_size`: Some(320022), added: 322497, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Presale::Contributions` (r:100 w:100)
|
||||
/// Proof: `Presale::Contributions` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Assets::Asset` (r:1 w:1)
|
||||
/// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Assets::Account` (r:101 w:101)
|
||||
/// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
|
||||
/// Storage: `AssetsFreezer::FrozenBalances` (r:1 w:0)
|
||||
/// Proof: `AssetsFreezer::FrozenBalances` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
|
||||
/// The range of component `n` is `[1, 100]`.
|
||||
fn batch_refund_failed_presale(n: u32, ) -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `1244 + n * (198 ±0)`
|
||||
// Estimated: `323487 + n * (2609 ±0)`
|
||||
// Minimum execution time: 70_612_000 picoseconds.
|
||||
Weight::from_parts(17_304_947, 323487)
|
||||
// Standard Error: 88_572
|
||||
.saturating_add(Weight::from_parts(42_218_217, 0).saturating_mul(n.into()))
|
||||
.saturating_add(T::DbWeight::get().reads(5_u64))
|
||||
.saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(n.into())))
|
||||
.saturating_add(T::DbWeight::get().writes(2_u64))
|
||||
.saturating_add(T::DbWeight::get().writes((2_u64).saturating_mul(n.into())))
|
||||
.saturating_add(Weight::from_parts(0, 2609).saturating_mul(n.into()))
|
||||
}
|
||||
}
|
||||
|
||||
// For backwards compatibility and tests.
|
||||
impl WeightInfo for () {
|
||||
/// Storage: `Presale::NextPresaleId` (r:1 w:1)
|
||||
/// Proof: `Presale::NextPresaleId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Presale::Presales` (r:0 w:1)
|
||||
/// Proof: `Presale::Presales` (`max_values`: None, `max_size`: Some(252), added: 2727, mode: `MaxEncodedLen`)
|
||||
fn create_presale() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `147`
|
||||
// Estimated: `1489`
|
||||
// Minimum execution time: 9_638_000 picoseconds.
|
||||
Weight::from_parts(10_003_000, 1489)
|
||||
.saturating_add(RocksDbWeight::get().reads(1_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(2_u64))
|
||||
}
|
||||
/// Storage: `Presale::Presales` (r:1 w:1)
|
||||
/// Proof: `Presale::Presales` (`max_values`: None, `max_size`: Some(252), added: 2727, mode: `MaxEncodedLen`)
|
||||
fn cancel_presale() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `356`
|
||||
// Estimated: `3717`
|
||||
// Minimum execution time: 12_202_000 picoseconds.
|
||||
Weight::from_parts(12_492_000, 3717)
|
||||
.saturating_add(RocksDbWeight::get().reads(1_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(1_u64))
|
||||
}
|
||||
/// Storage: `Presale::Presales` (r:1 w:0)
|
||||
/// Proof: `Presale::Presales` (`max_values`: None, `max_size`: Some(252), added: 2727, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Presale::WhitelistedAccounts` (r:0 w:1)
|
||||
/// Proof: `Presale::WhitelistedAccounts` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`)
|
||||
fn add_to_whitelist() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `356`
|
||||
// Estimated: `3717`
|
||||
// Minimum execution time: 12_973_000 picoseconds.
|
||||
Weight::from_parts(14_077_000, 3717)
|
||||
.saturating_add(RocksDbWeight::get().reads(1_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(1_u64))
|
||||
}
|
||||
/// Storage: `Presale::Presales` (r:1 w:0)
|
||||
/// Proof: `Presale::Presales` (`max_values`: None, `max_size`: Some(252), added: 2727, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Presale::Contributions` (r:1 w:1)
|
||||
/// Proof: `Presale::Contributions` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Presale::TotalRaised` (r:1 w:1)
|
||||
/// Proof: `Presale::TotalRaised` (`max_values`: None, `max_size`: Some(36), added: 2511, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Assets::Asset` (r:1 w:1)
|
||||
/// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Assets::Account` (r:4 w:4)
|
||||
/// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
|
||||
/// Storage: `AssetsFreezer::FrozenBalances` (r:1 w:0)
|
||||
/// Proof: `AssetsFreezer::FrozenBalances` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
|
||||
/// Storage: `System::Account` (r:1 w:1)
|
||||
/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Presale::Contributors` (r:1 w:1)
|
||||
/// Proof: `Presale::Contributors` (`max_values`: None, `max_size`: Some(320022), added: 322497, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Presale::TotalPlatformVolume` (r:1 w:1)
|
||||
/// Proof: `Presale::TotalPlatformVolume` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Presale::TotalPlatformFees` (r:1 w:1)
|
||||
/// Proof: `Presale::TotalPlatformFees` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`)
|
||||
fn contribute() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `1169`
|
||||
// Estimated: `323487`
|
||||
// Minimum execution time: 160_665_000 picoseconds.
|
||||
Weight::from_parts(165_629_000, 323487)
|
||||
.saturating_add(RocksDbWeight::get().reads(13_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(11_u64))
|
||||
}
|
||||
/// Storage: `Presale::Presales` (r:1 w:0)
|
||||
/// Proof: `Presale::Presales` (`max_values`: None, `max_size`: Some(252), added: 2727, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Presale::Contributions` (r:1 w:1)
|
||||
/// Proof: `Presale::Contributions` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Assets::Asset` (r:1 w:1)
|
||||
/// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Assets::Account` (r:4 w:4)
|
||||
/// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
|
||||
/// Storage: `AssetsFreezer::FrozenBalances` (r:1 w:1)
|
||||
/// Proof: `AssetsFreezer::FrozenBalances` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
|
||||
/// Storage: `System::Account` (r:1 w:1)
|
||||
/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
|
||||
/// Storage: `AssetsFreezer::Freezes` (r:1 w:1)
|
||||
/// Proof: `AssetsFreezer::Freezes` (`max_values`: None, `max_size`: Some(105), added: 2580, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Presale::TotalRaised` (r:1 w:1)
|
||||
/// Proof: `Presale::TotalRaised` (`max_values`: None, `max_size`: Some(36), added: 2511, mode: `MaxEncodedLen`)
|
||||
fn refund() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `1531`
|
||||
// Estimated: `11426`
|
||||
// Minimum execution time: 169_420_000 picoseconds.
|
||||
Weight::from_parts(173_774_000, 11426)
|
||||
.saturating_add(RocksDbWeight::get().reads(11_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(10_u64))
|
||||
}
|
||||
/// Storage: `Presale::Presales` (r:1 w:0)
|
||||
/// Proof: `Presale::Presales` (`max_values`: None, `max_size`: Some(252), added: 2727, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Presale::Contributions` (r:1 w:0)
|
||||
/// Proof: `Presale::Contributions` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Presale::TotalRaised` (r:1 w:0)
|
||||
/// Proof: `Presale::TotalRaised` (`max_values`: None, `max_size`: Some(36), added: 2511, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Presale::VestingClaimed` (r:1 w:1)
|
||||
/// Proof: `Presale::VestingClaimed` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Assets::Asset` (r:1 w:1)
|
||||
/// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Assets::Account` (r:2 w:2)
|
||||
/// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
|
||||
/// Storage: `AssetsFreezer::FrozenBalances` (r:1 w:0)
|
||||
/// Proof: `AssetsFreezer::FrozenBalances` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
|
||||
fn claim_vested() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `1392`
|
||||
// Estimated: `6208`
|
||||
// Minimum execution time: 72_526_000 picoseconds.
|
||||
Weight::from_parts(74_529_000, 6208)
|
||||
.saturating_add(RocksDbWeight::get().reads(8_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(4_u64))
|
||||
}
|
||||
/// Storage: `Presale::Presales` (r:1 w:0)
|
||||
/// Proof: `Presale::Presales` (`max_values`: None, `max_size`: Some(252), added: 2727, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Presale::Contributors` (r:1 w:0)
|
||||
/// Proof: `Presale::Contributors` (`max_values`: None, `max_size`: Some(320022), added: 322497, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Presale::Contributions` (r:1 w:1)
|
||||
/// Proof: `Presale::Contributions` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Assets::Asset` (r:1 w:1)
|
||||
/// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Assets::Account` (r:2 w:2)
|
||||
/// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
|
||||
/// Storage: `AssetsFreezer::FrozenBalances` (r:1 w:0)
|
||||
/// Proof: `AssetsFreezer::FrozenBalances` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
|
||||
fn refund_cancelled_presale() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `1370`
|
||||
// Estimated: `323487`
|
||||
// Minimum execution time: 66_454_000 picoseconds.
|
||||
Weight::from_parts(69_615_000, 323487)
|
||||
.saturating_add(RocksDbWeight::get().reads(7_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(4_u64))
|
||||
}
|
||||
/// Storage: `Presale::Presales` (r:1 w:1)
|
||||
/// Proof: `Presale::Presales` (`max_values`: None, `max_size`: Some(252), added: 2727, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Presale::TotalRaised` (r:1 w:0)
|
||||
/// Proof: `Presale::TotalRaised` (`max_values`: None, `max_size`: Some(36), added: 2511, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Presale::Contributors` (r:1 w:0)
|
||||
/// Proof: `Presale::Contributors` (`max_values`: None, `max_size`: Some(320022), added: 322497, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Presale::Contributions` (r:100 w:0)
|
||||
/// Proof: `Presale::Contributions` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Assets::Asset` (r:1 w:1)
|
||||
/// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Assets::Account` (r:101 w:101)
|
||||
/// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
|
||||
/// Storage: `AssetsFreezer::FrozenBalances` (r:1 w:1)
|
||||
/// Proof: `AssetsFreezer::FrozenBalances` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
|
||||
/// Storage: `System::Account` (r:101 w:101)
|
||||
/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
|
||||
/// Storage: `AssetsFreezer::Freezes` (r:1 w:1)
|
||||
/// Proof: `AssetsFreezer::Freezes` (`max_values`: None, `max_size`: Some(105), added: 2580, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Presale::SuccessfulPresales` (r:1 w:1)
|
||||
/// Proof: `Presale::SuccessfulPresales` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
|
||||
/// The range of component `n` is `[1, 100]`.
|
||||
fn finalize_presale(n: u32, ) -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `525`
|
||||
// Estimated: `3717 + n * (3235 ±0)`
|
||||
// Minimum execution time: 17_663_000 picoseconds.
|
||||
Weight::from_parts(18_454_000, 3717)
|
||||
// Standard Error: 463_975
|
||||
.saturating_add(Weight::from_parts(2_728_899, 0).saturating_mul(n.into()))
|
||||
.saturating_add(RocksDbWeight::get().reads(2_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(1_u64))
|
||||
.saturating_add(Weight::from_parts(0, 3235).saturating_mul(n.into()))
|
||||
}
|
||||
/// Storage: `Presale::Presales` (r:1 w:0)
|
||||
/// Proof: `Presale::Presales` (`max_values`: None, `max_size`: Some(252), added: 2727, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Presale::Contributors` (r:1 w:0)
|
||||
/// Proof: `Presale::Contributors` (`max_values`: None, `max_size`: Some(320022), added: 322497, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Presale::Contributions` (r:100 w:100)
|
||||
/// Proof: `Presale::Contributions` (`max_values`: None, `max_size`: Some(110), added: 2585, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Assets::Asset` (r:1 w:1)
|
||||
/// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Assets::Account` (r:101 w:101)
|
||||
/// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
|
||||
/// Storage: `AssetsFreezer::FrozenBalances` (r:1 w:0)
|
||||
/// Proof: `AssetsFreezer::FrozenBalances` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
|
||||
/// The range of component `n` is `[1, 100]`.
|
||||
fn batch_refund_failed_presale(n: u32, ) -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `1244 + n * (198 ±0)`
|
||||
// Estimated: `323487 + n * (2609 ±0)`
|
||||
// Minimum execution time: 70_612_000 picoseconds.
|
||||
Weight::from_parts(17_304_947, 323487)
|
||||
// Standard Error: 88_572
|
||||
.saturating_add(Weight::from_parts(42_218_217, 0).saturating_mul(n.into()))
|
||||
.saturating_add(RocksDbWeight::get().reads(5_u64))
|
||||
.saturating_add(RocksDbWeight::get().reads((2_u64).saturating_mul(n.into())))
|
||||
.saturating_add(RocksDbWeight::get().writes(2_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes((2_u64).saturating_mul(n.into())))
|
||||
.saturating_add(Weight::from_parts(0, 2609).saturating_mul(n.into()))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
[package]
|
||||
name = "pezpallet-referral"
|
||||
version = "1.0.0"
|
||||
description = "PezkuwiChain Referral Management Pallet"
|
||||
authors.workspace = true
|
||||
homepage.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
publish = false
|
||||
repository.workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
||||
[dependencies]
|
||||
codec = { default-features = false, features = ["derive"], workspace = true }
|
||||
pezframe-benchmarking = { optional = true, workspace = true }
|
||||
pezframe-support = { default-features = false, workspace = true }
|
||||
pezframe-system = { default-features = false, workspace = true }
|
||||
log = { default-features = false, workspace = true }
|
||||
pezpallet-identity-kyc = { workspace = true, default-features = false }
|
||||
scale-info = { default-features = false, features = [
|
||||
"derive",
|
||||
], workspace = true }
|
||||
pezsp-runtime = { default-features = false, workspace = true }
|
||||
pezsp-std = { default-features = false, workspace = true }
|
||||
|
||||
# Projemizin özel tiplerini ve trait'lerini içeren kütüphane
|
||||
pezkuwi-primitives = { workspace = true, default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
pezpallet-balances = { workspace = true }
|
||||
pezpallet-nfts = { workspace = true }
|
||||
pezsp-core = { workspace = true }
|
||||
pezsp-io = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = [
|
||||
"codec/std",
|
||||
"pezframe-benchmarking?/std",
|
||||
"pezframe-support/std",
|
||||
"pezframe-system/std",
|
||||
"log/std",
|
||||
"pezpallet-balances/std",
|
||||
"pezpallet-identity-kyc/std",
|
||||
"pezpallet-nfts/std",
|
||||
"pezkuwi-primitives/std",
|
||||
"scale-info/std",
|
||||
"pezsp-core/std",
|
||||
"pezsp-io/std",
|
||||
"pezsp-runtime/std",
|
||||
"pezsp-std/std",
|
||||
]
|
||||
runtime-benchmarks = [
|
||||
"pezframe-benchmarking",
|
||||
"pezframe-benchmarking/runtime-benchmarks",
|
||||
"pezframe-support/runtime-benchmarks",
|
||||
"pezframe-system/runtime-benchmarks",
|
||||
"pezpallet-balances/runtime-benchmarks",
|
||||
"pezpallet-identity-kyc/runtime-benchmarks",
|
||||
"pezpallet-nfts/runtime-benchmarks",
|
||||
"pezkuwi-primitives/runtime-benchmarks",
|
||||
"pezsp-io/runtime-benchmarks",
|
||||
"pezsp-runtime/runtime-benchmarks",
|
||||
]
|
||||
try-runtime = [
|
||||
"pezframe-support/try-runtime",
|
||||
"pezframe-system/try-runtime",
|
||||
"pezpallet-balances/try-runtime",
|
||||
"pezpallet-identity-kyc/try-runtime",
|
||||
"pezpallet-nfts/try-runtime",
|
||||
"pezsp-runtime/try-runtime",
|
||||
]
|
||||
@@ -0,0 +1,47 @@
|
||||
//! Benchmarking setup for pezpallet-referral
|
||||
|
||||
#![cfg(feature = "runtime-benchmarks")]
|
||||
|
||||
use super::*;
|
||||
use crate::Pallet as Referral;
|
||||
use pezframe_benchmarking::v2::*;
|
||||
use pezframe_system::RawOrigin;
|
||||
|
||||
#[benchmarks]
|
||||
mod benchmarks {
|
||||
use super::*;
|
||||
|
||||
#[benchmark]
|
||||
fn initiate_referral() {
|
||||
let referrer: T::AccountId = account("referrer", 0, 0);
|
||||
let referred: T::AccountId = account("referred", 0, 1);
|
||||
|
||||
// Ensure the `referred` account has not been referred before
|
||||
PendingReferrals::<T>::remove(&referred);
|
||||
Referrals::<T>::remove(&referred);
|
||||
|
||||
#[extrinsic_call]
|
||||
initiate_referral(RawOrigin::Signed(referrer.clone()), referred.clone());
|
||||
|
||||
assert_eq!(PendingReferrals::<T>::get(&referred), Some(referrer));
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn force_confirm_referral() {
|
||||
let referrer: T::AccountId = account("referrer", 0, 0);
|
||||
let referred: T::AccountId = account("referred", 0, 1);
|
||||
|
||||
// Ensure clean state
|
||||
PendingReferrals::<T>::remove(&referred);
|
||||
Referrals::<T>::remove(&referred);
|
||||
ReferralCount::<T>::remove(&referrer);
|
||||
|
||||
#[extrinsic_call]
|
||||
force_confirm_referral(RawOrigin::Root, referrer.clone(), referred.clone());
|
||||
|
||||
assert!(Referrals::<T>::contains_key(&referred));
|
||||
assert_eq!(ReferralCount::<T>::get(&referrer), 1);
|
||||
}
|
||||
|
||||
impl_benchmark_test_suite!(Referral, crate::mock::new_test_ext(), crate::mock::Test);
|
||||
}
|
||||
@@ -0,0 +1,380 @@
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
//! # Referral Pallet
|
||||
//!
|
||||
//! A pallet for managing user referrals and tracking network growth through invitation mechanics.
|
||||
//!
|
||||
//! ## Overview
|
||||
//!
|
||||
//! The Referral pallet implements a referral system that incentivizes user growth by tracking
|
||||
//! and rewarding users who successfully invite others to complete KYC verification. Referral
|
||||
//! counts contribute to trust scores and validator eligibility.
|
||||
//!
|
||||
//! ## Referral Workflow
|
||||
//!
|
||||
//! ### Initiation Phase
|
||||
//!
|
||||
//! 1. User A calls `initiate_referral(user_b_account)` to invite User B
|
||||
//! 2. System creates a pending referral record linking B to A
|
||||
//! 3. User B must not have been referred by anyone else
|
||||
//! 4. Self-referral is prevented
|
||||
//!
|
||||
//! ### Confirmation Phase
|
||||
//!
|
||||
//! 1. User B completes identity registration and KYC application
|
||||
//! 2. KYC authority approves User B's application
|
||||
//! 3. `OnKycApproved` hook automatically fires
|
||||
//! 4. System:
|
||||
//! - Converts pending referral to confirmed referral
|
||||
//! - Increments User A's referral count
|
||||
//! - Records block number of confirmation
|
||||
//! - Emits `ReferralConfirmed` event
|
||||
//!
|
||||
//! ## Referral Score System
|
||||
//!
|
||||
//! The referral count contributes to the trust score calculation in `pezpallet-trust`:
|
||||
//! - Each successful referral increases the referrer's reputation
|
||||
//! - Referral count is used by `ReferralScoreProvider` trait
|
||||
//! - Higher referral counts improve validator pool eligibility
|
||||
//! - Community validators require active referral participation
|
||||
//!
|
||||
//! ## Security Features
|
||||
//!
|
||||
//! - **One Referrer Per User**: Each user can only be referred once
|
||||
//! - **No Self-Referral**: Users cannot refer themselves
|
||||
//! - **KYC Verification Required**: Referrals only count after KYC approval
|
||||
//! - **Immutable History**: Confirmed referrals cannot be changed
|
||||
//! - **Block Number Recording**: Transparent audit trail
|
||||
//!
|
||||
//! ## Interface
|
||||
//!
|
||||
//! ### User Extrinsics
|
||||
//!
|
||||
//! - `initiate_referral(referred)` - Invite a new user to the ecosystem
|
||||
//!
|
||||
//! ### Storage
|
||||
//!
|
||||
//! - `PendingReferrals` - Invited users awaiting KYC approval (referred → referrer)
|
||||
//! - `ReferralCount` - Number of successful referrals per user (referrer → count)
|
||||
//! - `Referrals` - Confirmed referral records with metadata (referred → ReferralInfo)
|
||||
//!
|
||||
//! ### Trait Implementations
|
||||
//!
|
||||
//! - `OnKycApproved` - Hook called by `pezpallet-identity-kyc` upon KYC approval
|
||||
//! - `ReferralScoreProvider` - Query interface for trust score calculation
|
||||
//! - `InviterProvider` - Query who referred a specific user
|
||||
//!
|
||||
//! ## Integration Points
|
||||
//!
|
||||
//! ### With pezpallet-identity-kyc
|
||||
//! - Listens for KYC approval events via `OnKycApproved` hook
|
||||
//! - Automatically confirms pending referrals upon approval
|
||||
//!
|
||||
//! ### With pezpallet-trust
|
||||
//! - Provides referral scores for composite trust calculation
|
||||
//! - Contributes to overall reputation metrics
|
||||
//!
|
||||
//! ### With pezpallet-validator-pool
|
||||
//! - Community validator category requires referral participation
|
||||
//! - Referral count affects pool eligibility
|
||||
//!
|
||||
//! ## Runtime Integration Example
|
||||
//!
|
||||
//! ```ignore
|
||||
//! impl pezpallet_referral::Config for Runtime {
|
||||
//! type RuntimeEvent = RuntimeEvent;
|
||||
//! type WeightInfo = pezpallet_referral::weights::BizinikiwiWeight<Runtime>;
|
||||
//! }
|
||||
//!
|
||||
//! // Configure pezpallet-identity-kyc to notify referral pallet
|
||||
//! impl pezpallet_identity_kyc::Config for Runtime {
|
||||
//! // ...
|
||||
//! type OnKycApproved = Referral; // Hook referral confirmation
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
pub use pallet::*;
|
||||
#[cfg(test)]
|
||||
mod mock;
|
||||
pub mod types; // Adding our new types module
|
||||
pub mod weights;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
mod benchmarking;
|
||||
extern crate alloc;
|
||||
use crate::weights::WeightInfo;
|
||||
|
||||
#[pezframe_support::pallet]
|
||||
pub mod pallet {
|
||||
use super::*;
|
||||
use crate::types::{InviterProvider, RawScore, ReferralScoreProvider, ReferrerStats};
|
||||
use pezframe_support::pezpallet_prelude::*;
|
||||
use pezframe_system::pezpallet_prelude::*;
|
||||
use pezpallet_identity_kyc::types::{KycStatus, OnCitizenshipRevoked, OnKycApproved};
|
||||
|
||||
#[pallet::pallet]
|
||||
pub struct Pallet<T>(_);
|
||||
|
||||
#[pallet::config]
|
||||
pub trait Config: pezframe_system::Config + pezpallet_identity_kyc::Config + TypeInfo {
|
||||
type RuntimeEvent: From<Event<Self>> + IsType<<Self as pezframe_system::Config>::RuntimeEvent>;
|
||||
type WeightInfo: weights::WeightInfo;
|
||||
|
||||
/// Default referrer account - used when no referrer is specified
|
||||
/// This allows automatic assignment of founder as referrer for users without invitations
|
||||
type DefaultReferrer: Get<Self::AccountId>;
|
||||
|
||||
/// Penalty score per revoked referral
|
||||
/// DIRECT RESPONSIBILITY: Bad referrals reduce referrer's score
|
||||
/// Default: 3 (each bad referral costs 3x a good referral)
|
||||
#[pallet::constant]
|
||||
type PenaltyPerRevocation: Get<u32>;
|
||||
}
|
||||
|
||||
// --- Storage Items ---
|
||||
|
||||
/// Holds users awaiting to join system via referral.
|
||||
/// (Referred AccountId -> Referrer AccountId)
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn pending_referrals)]
|
||||
pub type PendingReferrals<T: Config> =
|
||||
StorageMap<_, Blake2_128Concat, T::AccountId, T::AccountId, OptionQuery>;
|
||||
|
||||
/// Holds successfully completed referral count per user.
|
||||
/// (Referrer AccountId -> Count)
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn referral_count)]
|
||||
pub type ReferralCount<T: Config> =
|
||||
StorageMap<_, Blake2_128Concat, T::AccountId, u32, ValueQuery>;
|
||||
|
||||
/// Holds who a user invited and transaction details.
|
||||
/// (Referred AccountId -> ReferralInfo)
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn referrals)]
|
||||
pub type Referrals<T: Config> =
|
||||
StorageMap<_, Blake2_128Concat, T::AccountId, ReferralInfo<T>, OptionQuery>;
|
||||
|
||||
/// Referrer statistics for direct responsibility tracking
|
||||
/// ACCOUNTABILITY: Tracks good and bad referrals for penalty calculation
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn referrer_stats)]
|
||||
pub type ReferrerStatsStorage<T: Config> =
|
||||
StorageMap<_, Blake2_128Concat, T::AccountId, ReferrerStats, ValueQuery>;
|
||||
|
||||
#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
|
||||
pub struct ReferralInfo<T: Config> {
|
||||
pub referrer: T::AccountId,
|
||||
pub created_at: BlockNumberFor<T>,
|
||||
}
|
||||
|
||||
// --- Events ---
|
||||
#[pallet::event]
|
||||
#[pallet::generate_deposit(pub(super) fn deposit_event)]
|
||||
pub enum Event<T: Config> {
|
||||
/// When a user invites another user.
|
||||
ReferralInitiated { referrer: T::AccountId, referred: T::AccountId },
|
||||
/// When invited user successfully completes KYC process.
|
||||
ReferralConfirmed {
|
||||
referrer: T::AccountId,
|
||||
referred: T::AccountId,
|
||||
new_referrer_count: u32,
|
||||
},
|
||||
/// When a referral is penalized due to revoked citizenship
|
||||
/// DIRECT RESPONSIBILITY: Only the referrer is affected
|
||||
ReferralPenalized {
|
||||
referrer: T::AccountId,
|
||||
revoked_citizen: T::AccountId,
|
||||
new_penalty_score: u32,
|
||||
total_revoked: u32,
|
||||
},
|
||||
}
|
||||
|
||||
// --- Errors ---
|
||||
#[pallet::error]
|
||||
pub enum Error<T> {
|
||||
/// A user cannot invite themselves.
|
||||
SelfReferral,
|
||||
/// This user has already been invited by someone else.
|
||||
AlreadyReferred,
|
||||
}
|
||||
|
||||
// --- Extrinsics ---
|
||||
#[pallet::call]
|
||||
impl<T: Config> Pallet<T> {
|
||||
/// Initiates a referral record to invite another user to the system.
|
||||
#[pallet::call_index(0)]
|
||||
#[pallet::weight(<T as Config>::WeightInfo::initiate_referral())]
|
||||
pub fn initiate_referral(origin: OriginFor<T>, referred: T::AccountId) -> DispatchResult {
|
||||
let referrer = ensure_signed(origin)?;
|
||||
ensure!(referrer != referred, Error::<T>::SelfReferral);
|
||||
ensure!(!Referrals::<T>::contains_key(&referred), Error::<T>::AlreadyReferred);
|
||||
ensure!(!PendingReferrals::<T>::contains_key(&referred), Error::<T>::AlreadyReferred);
|
||||
|
||||
PendingReferrals::<T>::insert(&referred, &referrer);
|
||||
Self::deposit_event(Event::ReferralInitiated { referrer, referred });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sudo-only extrinsic to manually confirm a referral (for fixing historical data).
|
||||
/// This bypasses the normal KYC approval flow and directly confirms the referral.
|
||||
#[pallet::call_index(1)]
|
||||
#[pallet::weight(<T as Config>::WeightInfo::force_confirm_referral())]
|
||||
pub fn force_confirm_referral(
|
||||
origin: OriginFor<T>,
|
||||
referrer: T::AccountId,
|
||||
referred: T::AccountId,
|
||||
) -> DispatchResult {
|
||||
ensure_root(origin)?;
|
||||
ensure!(referrer != referred, Error::<T>::SelfReferral);
|
||||
ensure!(!Referrals::<T>::contains_key(&referred), Error::<T>::AlreadyReferred);
|
||||
|
||||
// Increment referrer's count
|
||||
let new_count = ReferralCount::<T>::get(&referrer).saturating_add(1);
|
||||
ReferralCount::<T>::insert(&referrer, new_count);
|
||||
|
||||
// Create and store referral info
|
||||
let referral_info = ReferralInfo {
|
||||
referrer: referrer.clone(),
|
||||
created_at: pezframe_system::Pallet::<T>::block_number(),
|
||||
};
|
||||
Referrals::<T>::insert(referred.clone(), referral_info);
|
||||
|
||||
// Remove from pending if it exists
|
||||
PendingReferrals::<T>::remove(&referred);
|
||||
|
||||
// Emit event
|
||||
Self::deposit_event(Event::ReferralConfirmed {
|
||||
referrer,
|
||||
referred,
|
||||
new_referrer_count: new_count,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// --- Trait Implementations ---
|
||||
|
||||
impl<T: Config> OnKycApproved<T::AccountId> for Pallet<T> {
|
||||
fn on_kyc_approved(who: &T::AccountId, referrer: &T::AccountId) {
|
||||
// Security check: Verify on-chain that the user's KYC status is actually
|
||||
// "Approved" before confirming the referral.
|
||||
if pezpallet_identity_kyc::Pallet::<T>::get_kyc_status(who) ==
|
||||
pezpallet_identity_kyc::types::KycLevel::Approved
|
||||
{
|
||||
// Check if this referral already exists (prevent double-counting)
|
||||
if Referrals::<T>::contains_key(who) {
|
||||
return; // Already processed
|
||||
}
|
||||
|
||||
// UPDATED (Gemini suggestion): Use referrer from parameter directly
|
||||
// This ensures data consistency between identity-kyc and referral pallets
|
||||
// Previously we looked up from storage which could cause data loss
|
||||
|
||||
// Clean up legacy PendingReferrals if exists
|
||||
PendingReferrals::<T>::remove(who);
|
||||
|
||||
// Increment referrer's count
|
||||
let new_count = ReferralCount::<T>::get(referrer).saturating_add(1);
|
||||
ReferralCount::<T>::insert(referrer, new_count);
|
||||
|
||||
// Update referrer stats for direct responsibility tracking
|
||||
ReferrerStatsStorage::<T>::mutate(referrer, |stats| {
|
||||
stats.total_referrals = stats.total_referrals.saturating_add(1);
|
||||
});
|
||||
|
||||
// Create and store referral info
|
||||
let referral_info = ReferralInfo {
|
||||
referrer: referrer.clone(),
|
||||
created_at: pezframe_system::Pallet::<T>::block_number(),
|
||||
};
|
||||
Referrals::<T>::insert(who.clone(), referral_info);
|
||||
|
||||
// Emit confirmation event
|
||||
Self::deposit_event(Event::ReferralConfirmed {
|
||||
referrer: referrer.clone(),
|
||||
referred: who.clone(),
|
||||
new_referrer_count: new_count,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Implementation for direct responsibility penalty system
|
||||
/// Called when a citizen's status is revoked (malicious actor identified)
|
||||
impl<T: Config> OnCitizenshipRevoked<T::AccountId> for Pallet<T> {
|
||||
fn on_citizenship_revoked(who: &T::AccountId) {
|
||||
// Find the referrer of the revoked citizen
|
||||
if let Some(referral_info) = Referrals::<T>::get(who) {
|
||||
let referrer = referral_info.referrer;
|
||||
let penalty_per_revocation = T::PenaltyPerRevocation::get();
|
||||
|
||||
// Update referrer stats - DIRECT RESPONSIBILITY
|
||||
// Only the direct referrer is penalized, not the chain
|
||||
ReferrerStatsStorage::<T>::mutate(&referrer, |stats| {
|
||||
stats.revoked_referrals = stats.revoked_referrals.saturating_add(1);
|
||||
stats.penalty_score =
|
||||
stats.penalty_score.saturating_add(penalty_per_revocation);
|
||||
});
|
||||
|
||||
let updated_stats = ReferrerStatsStorage::<T>::get(&referrer);
|
||||
|
||||
// Emit penalty event
|
||||
Self::deposit_event(Event::ReferralPenalized {
|
||||
referrer,
|
||||
revoked_citizen: who.clone(),
|
||||
new_penalty_score: updated_stats.penalty_score,
|
||||
total_revoked: updated_stats.revoked_referrals,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> ReferralScoreProvider<T::AccountId> for Pallet<T> {
|
||||
type Score = RawScore;
|
||||
|
||||
fn get_referral_score(who: &T::AccountId) -> RawScore {
|
||||
let stats = ReferrerStatsStorage::<T>::get(who);
|
||||
|
||||
// Calculate good referrals (total minus revoked)
|
||||
let good_referrals = stats.total_referrals.saturating_sub(stats.revoked_referrals);
|
||||
|
||||
// BALANCED PENALTY SYSTEM (Gemini's suggestion):
|
||||
// "Every 4 bad referrals = -10 points"
|
||||
// This is equivalent to "1 bad = -2.5 points"
|
||||
// Much fairer than "1 bad = 3 good deleted"
|
||||
//
|
||||
// Formula: penalty_points = (revoked_referrals / 4) * 10
|
||||
// Simplified: penalty_points = revoked_referrals * 10 / 4 = revoked_referrals * 2.5
|
||||
// Using integer math: penalty_points = (revoked_referrals * 10) / 4
|
||||
let penalty_points = (stats.revoked_referrals.saturating_mul(10)) / 4;
|
||||
|
||||
// Calculate base score from good referrals
|
||||
// Scoring system with max 500 points:
|
||||
// 0 referrals = 0 points
|
||||
// 1-10 referrals = count * 10 points (10, 20, 30, ..., 100)
|
||||
// 11-50 referrals = 100 + ((count - 10) * 5) = 105, 110, ..., 300
|
||||
// 51-100 referrals = 300 + ((count - 50) * 4) = 304, 308, ..., 500
|
||||
// 101+ referrals = 500 points (maximum)
|
||||
let base_score = match good_referrals {
|
||||
0 => 0,
|
||||
1..=10 => good_referrals * 10,
|
||||
11..=50 => 100 + ((good_referrals - 10) * 5),
|
||||
51..=100 => 300 + ((good_referrals - 50) * 4),
|
||||
_ => 500,
|
||||
};
|
||||
|
||||
// Apply penalty (cannot go below 0)
|
||||
base_score.saturating_sub(penalty_points)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> InviterProvider<T::AccountId> for Pallet<T> {
|
||||
fn get_inviter(who: &T::AccountId) -> Option<T::AccountId> {
|
||||
Referrals::<T>::get(who).map(|info| info.referrer)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
// pezkuwi/pallets/referral/src/mock.rs (Updated for new trustless model)
|
||||
|
||||
use crate as pezpallet_referral;
|
||||
use pezframe_support::{
|
||||
construct_runtime, derive_impl, parameter_types,
|
||||
traits::{ConstU128, ConstU32},
|
||||
};
|
||||
use pezframe_system::EnsureRoot;
|
||||
use pezsp_core::H256;
|
||||
use pezsp_runtime::BuildStorage;
|
||||
|
||||
type Block = pezframe_system::mocking::MockBlock<Test>;
|
||||
pub type AccountId = u64;
|
||||
pub type Balance = u128;
|
||||
|
||||
// Test accounts
|
||||
pub const FOUNDER: AccountId = 100;
|
||||
pub const REFERRER: AccountId = 1;
|
||||
pub const REFERRED: AccountId = 2;
|
||||
pub const USER_3: AccountId = 3;
|
||||
|
||||
construct_runtime!(
|
||||
pub enum Test
|
||||
{
|
||||
System: pezframe_system,
|
||||
Balances: pezpallet_balances,
|
||||
IdentityKyc: pezpallet_identity_kyc,
|
||||
Referral: pezpallet_referral,
|
||||
}
|
||||
);
|
||||
|
||||
#[derive_impl(pezframe_system::config_preludes::TestDefaultConfig)]
|
||||
impl pezframe_system::Config for Test {
|
||||
type Block = Block;
|
||||
type AccountData = pezpallet_balances::AccountData<Balance>;
|
||||
}
|
||||
|
||||
#[derive_impl(pezpallet_balances::config_preludes::TestDefaultConfig)]
|
||||
impl pezpallet_balances::Config for Test {
|
||||
type Balance = Balance;
|
||||
type ExistentialDeposit = ConstU128<1>;
|
||||
type AccountStore = System;
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const KycApplicationDepositAmount: Balance = 100;
|
||||
pub const MaxStringLen: u32 = 50;
|
||||
pub const MaxCidLen: u32 = 128;
|
||||
pub const PenaltyPerRevocationAmount: u32 = 3;
|
||||
}
|
||||
|
||||
// Mock implementation for CitizenNftProvider
|
||||
pub struct MockCitizenNftProvider;
|
||||
impl pezpallet_identity_kyc::types::CitizenNftProvider<AccountId> for MockCitizenNftProvider {
|
||||
fn mint_citizen_nft(_who: &AccountId) -> pezsp_runtime::DispatchResult {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn mint_citizen_nft_confirmed(_who: &AccountId) -> pezsp_runtime::DispatchResult {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn burn_citizen_nft(_who: &AccountId) -> pezsp_runtime::DispatchResult {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl pezpallet_identity_kyc::Config for Test {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type Currency = Balances;
|
||||
type GovernanceOrigin = EnsureRoot<AccountId>;
|
||||
type WeightInfo = ();
|
||||
type OnKycApproved = Referral; // Referral pallet handles KYC approval hook
|
||||
type OnCitizenshipRevoked = Referral; // Referral pallet handles revocation penalty
|
||||
type CitizenNftProvider = MockCitizenNftProvider;
|
||||
type KycApplicationDeposit = KycApplicationDepositAmount;
|
||||
type MaxStringLength = MaxStringLen;
|
||||
type MaxCidLength = MaxCidLen;
|
||||
}
|
||||
|
||||
// Default referrer for testing (founder account)
|
||||
pub struct DefaultReferrerAccount;
|
||||
impl pezframe_support::traits::Get<AccountId> for DefaultReferrerAccount {
|
||||
fn get() -> AccountId {
|
||||
FOUNDER
|
||||
}
|
||||
}
|
||||
|
||||
impl pezpallet_referral::Config for Test {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type WeightInfo = ();
|
||||
type DefaultReferrer = DefaultReferrerAccount;
|
||||
type PenaltyPerRevocation = PenaltyPerRevocationAmount;
|
||||
}
|
||||
|
||||
/// Build test externalities with founding citizens
|
||||
pub fn new_test_ext() -> pezsp_io::TestExternalities {
|
||||
let mut t = pezframe_system::GenesisConfig::<Test>::default().build_storage().unwrap();
|
||||
|
||||
pezpallet_balances::GenesisConfig::<Test> {
|
||||
balances: vec![
|
||||
(FOUNDER, 1_000_000),
|
||||
(REFERRER, 10_000),
|
||||
(REFERRED, 10_000),
|
||||
(USER_3, 10_000),
|
||||
],
|
||||
..Default::default()
|
||||
}
|
||||
.assimilate_storage(&mut t)
|
||||
.unwrap();
|
||||
|
||||
// Add founding citizens via genesis config
|
||||
pezpallet_identity_kyc::GenesisConfig::<Test> {
|
||||
founding_citizens: vec![
|
||||
(FOUNDER, H256::from_low_u64_be(1)),
|
||||
(REFERRER, H256::from_low_u64_be(2)),
|
||||
],
|
||||
_phantom: Default::default(),
|
||||
}
|
||||
.assimilate_storage(&mut t)
|
||||
.unwrap();
|
||||
|
||||
let mut ext = pezsp_io::TestExternalities::new(t);
|
||||
ext.execute_with(|| System::set_block_number(1));
|
||||
ext
|
||||
}
|
||||
|
||||
/// Build test externalities for penalty tests (needs revoked citizens)
|
||||
pub fn new_test_ext_with_citizens() -> pezsp_io::TestExternalities {
|
||||
new_test_ext()
|
||||
}
|
||||
@@ -0,0 +1,538 @@
|
||||
use crate::{
|
||||
mock::*, pallet::ReferralInfo, Error, Event, PendingReferrals, ReferralCount, Referrals,
|
||||
ReferrerStatsStorage,
|
||||
};
|
||||
use pezframe_support::{assert_noop, assert_ok};
|
||||
use pezpallet_identity_kyc::types::{OnCitizenshipRevoked, OnKycApproved};
|
||||
use pezsp_runtime::DispatchError;
|
||||
|
||||
type ReferralPallet = crate::Pallet<Test>;
|
||||
|
||||
// ============================================================================
|
||||
// initiate_referral Tests
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn initiate_referral_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// REFERRER (citizen) invites REFERRED
|
||||
assert_ok!(ReferralPallet::initiate_referral(RuntimeOrigin::signed(REFERRER), REFERRED));
|
||||
|
||||
// Verification: Correct record is added to pending referrals list.
|
||||
assert_eq!(ReferralPallet::pending_referrals(REFERRED), Some(REFERRER));
|
||||
|
||||
// Correct event is emitted.
|
||||
System::assert_last_event(
|
||||
Event::ReferralInitiated { referrer: REFERRER, referred: REFERRED }.into(),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn initiate_referral_fails_for_self_referral() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// User cannot invite themselves.
|
||||
assert_noop!(
|
||||
ReferralPallet::initiate_referral(RuntimeOrigin::signed(REFERRER), REFERRER),
|
||||
Error::<Test>::SelfReferral
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn initiate_referral_fails_if_already_referred() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// First referral succeeds
|
||||
assert_ok!(ReferralPallet::initiate_referral(RuntimeOrigin::signed(REFERRER), REFERRED));
|
||||
|
||||
// Second referral attempt by USER_3 fails
|
||||
assert_noop!(
|
||||
ReferralPallet::initiate_referral(RuntimeOrigin::signed(USER_3), REFERRED),
|
||||
Error::<Test>::AlreadyReferred
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// on_kyc_approved Hook Tests (Updated for new trait signature)
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn on_kyc_approved_hook_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// Setup: REFERRER invites REFERRED via PendingReferrals
|
||||
assert_ok!(ReferralPallet::initiate_referral(RuntimeOrigin::signed(REFERRER), REFERRED));
|
||||
|
||||
// Set user's KYC as approved
|
||||
pezpallet_identity_kyc::KycStatuses::<Test>::insert(
|
||||
REFERRED,
|
||||
pezpallet_identity_kyc::types::KycLevel::Approved,
|
||||
);
|
||||
|
||||
// Action: Call on_kyc_approved with referrer parameter
|
||||
ReferralPallet::on_kyc_approved(&REFERRED, &REFERRER);
|
||||
|
||||
// Verification
|
||||
// 1. Pending referral record is deleted
|
||||
assert_eq!(PendingReferrals::<Test>::get(REFERRED), None);
|
||||
// 2. Referrer's referral count increases by 1
|
||||
assert_eq!(ReferralCount::<Test>::get(REFERRER), 1);
|
||||
// 3. Permanent referral information is created
|
||||
assert!(Referrals::<Test>::contains_key(REFERRED));
|
||||
let referral_info = Referrals::<Test>::get(REFERRED).unwrap();
|
||||
assert_eq!(referral_info.referrer, REFERRER);
|
||||
// 4. ReferrerStats updated
|
||||
let stats = ReferrerStatsStorage::<Test>::get(REFERRER);
|
||||
assert_eq!(stats.total_referrals, 1);
|
||||
assert_eq!(stats.revoked_referrals, 0);
|
||||
// 5. Correct event is emitted
|
||||
System::assert_last_event(
|
||||
Event::ReferralConfirmed {
|
||||
referrer: REFERRER,
|
||||
referred: REFERRED,
|
||||
new_referrer_count: 1,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn on_kyc_approved_uses_referrer_parameter() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// No pending referral - but referrer is passed as parameter
|
||||
// This tests the new model where identity-kyc passes referrer directly
|
||||
|
||||
pezpallet_identity_kyc::KycStatuses::<Test>::insert(
|
||||
REFERRED,
|
||||
pezpallet_identity_kyc::types::KycLevel::Approved,
|
||||
);
|
||||
|
||||
// Call with explicit referrer parameter
|
||||
ReferralPallet::on_kyc_approved(&REFERRED, &REFERRER);
|
||||
|
||||
// Should use the passed referrer, not look up from PendingReferrals
|
||||
let referral_info = Referrals::<Test>::get(REFERRED).unwrap();
|
||||
assert_eq!(referral_info.referrer, REFERRER);
|
||||
assert_eq!(ReferralCount::<Test>::get(REFERRER), 1);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn on_kyc_approved_does_nothing_if_not_approved_status() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// User's KYC is NOT approved - status is still NotStarted
|
||||
// on_kyc_approved should do nothing
|
||||
|
||||
let initial_count = ReferralCount::<Test>::get(REFERRER);
|
||||
ReferralPallet::on_kyc_approved(&REFERRED, &REFERRER);
|
||||
|
||||
// No changes should have occurred
|
||||
assert_eq!(ReferralCount::<Test>::get(REFERRER), initial_count);
|
||||
assert!(Referrals::<Test>::get(REFERRED).is_none());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn on_kyc_approved_prevents_double_counting() {
|
||||
new_test_ext().execute_with(|| {
|
||||
pezpallet_identity_kyc::KycStatuses::<Test>::insert(
|
||||
REFERRED,
|
||||
pezpallet_identity_kyc::types::KycLevel::Approved,
|
||||
);
|
||||
|
||||
// First approval
|
||||
ReferralPallet::on_kyc_approved(&REFERRED, &REFERRER);
|
||||
assert_eq!(ReferralCount::<Test>::get(REFERRER), 1);
|
||||
|
||||
// Second approval attempt should be ignored (already processed)
|
||||
ReferralPallet::on_kyc_approved(&REFERRED, &REFERRER);
|
||||
assert_eq!(ReferralCount::<Test>::get(REFERRER), 1); // Still 1
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// on_citizenship_revoked Tests (Direct Responsibility Penalty)
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn on_citizenship_revoked_penalizes_referrer() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// Setup: Complete referral first
|
||||
pezpallet_identity_kyc::KycStatuses::<Test>::insert(
|
||||
REFERRED,
|
||||
pezpallet_identity_kyc::types::KycLevel::Approved,
|
||||
);
|
||||
ReferralPallet::on_kyc_approved(&REFERRED, &REFERRER);
|
||||
|
||||
// Verify initial stats
|
||||
let stats = ReferrerStatsStorage::<Test>::get(REFERRER);
|
||||
assert_eq!(stats.total_referrals, 1);
|
||||
assert_eq!(stats.revoked_referrals, 0);
|
||||
assert_eq!(stats.penalty_score, 0);
|
||||
|
||||
// Action: Citizenship revoked (malicious actor identified)
|
||||
ReferralPallet::on_citizenship_revoked(&REFERRED);
|
||||
|
||||
// Verify penalty applied
|
||||
let stats = ReferrerStatsStorage::<Test>::get(REFERRER);
|
||||
assert_eq!(stats.total_referrals, 1);
|
||||
assert_eq!(stats.revoked_referrals, 1);
|
||||
assert_eq!(stats.penalty_score, PenaltyPerRevocationAmount::get());
|
||||
|
||||
// Verify event
|
||||
System::assert_last_event(
|
||||
Event::ReferralPenalized {
|
||||
referrer: REFERRER,
|
||||
revoked_citizen: REFERRED,
|
||||
new_penalty_score: PenaltyPerRevocationAmount::get(),
|
||||
total_revoked: 1,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn on_citizenship_revoked_does_nothing_if_no_referral() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// Try to revoke someone who was never referred
|
||||
let unknown_user = 999;
|
||||
ReferralPallet::on_citizenship_revoked(&unknown_user);
|
||||
|
||||
// No penalty events should be emitted
|
||||
// (this is safe - just a no-op)
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Referral Score Calculation Tests (with balanced penalty)
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn referral_score_tier_0_to_10() {
|
||||
use crate::types::ReferralScoreProvider;
|
||||
|
||||
new_test_ext().execute_with(|| {
|
||||
// Update stats directly for testing
|
||||
ReferrerStatsStorage::<Test>::mutate(&REFERRER, |stats| {
|
||||
stats.total_referrals = 0;
|
||||
});
|
||||
assert_eq!(ReferralPallet::get_referral_score(&REFERRER), 0);
|
||||
|
||||
// 1 referral = 10 points
|
||||
ReferrerStatsStorage::<Test>::mutate(&REFERRER, |stats| {
|
||||
stats.total_referrals = 1;
|
||||
});
|
||||
assert_eq!(ReferralPallet::get_referral_score(&REFERRER), 10);
|
||||
|
||||
// 5 referrals = 50 points
|
||||
ReferrerStatsStorage::<Test>::mutate(&REFERRER, |stats| {
|
||||
stats.total_referrals = 5;
|
||||
});
|
||||
assert_eq!(ReferralPallet::get_referral_score(&REFERRER), 50);
|
||||
|
||||
// 10 referrals = 100 points
|
||||
ReferrerStatsStorage::<Test>::mutate(&REFERRER, |stats| {
|
||||
stats.total_referrals = 10;
|
||||
});
|
||||
assert_eq!(ReferralPallet::get_referral_score(&REFERRER), 100);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn referral_score_tier_11_to_50() {
|
||||
use crate::types::ReferralScoreProvider;
|
||||
|
||||
new_test_ext().execute_with(|| {
|
||||
// 11 referrals: 100 + (1 * 5) = 105
|
||||
ReferrerStatsStorage::<Test>::mutate(&REFERRER, |stats| {
|
||||
stats.total_referrals = 11;
|
||||
});
|
||||
assert_eq!(ReferralPallet::get_referral_score(&REFERRER), 105);
|
||||
|
||||
// 20 referrals: 100 + (10 * 5) = 150
|
||||
ReferrerStatsStorage::<Test>::mutate(&REFERRER, |stats| {
|
||||
stats.total_referrals = 20;
|
||||
});
|
||||
assert_eq!(ReferralPallet::get_referral_score(&REFERRER), 150);
|
||||
|
||||
// 50 referrals: 100 + (40 * 5) = 300
|
||||
ReferrerStatsStorage::<Test>::mutate(&REFERRER, |stats| {
|
||||
stats.total_referrals = 50;
|
||||
});
|
||||
assert_eq!(ReferralPallet::get_referral_score(&REFERRER), 300);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn referral_score_tier_51_to_100() {
|
||||
use crate::types::ReferralScoreProvider;
|
||||
|
||||
new_test_ext().execute_with(|| {
|
||||
// 51 referrals: 300 + (1 * 4) = 304
|
||||
ReferrerStatsStorage::<Test>::mutate(&REFERRER, |stats| {
|
||||
stats.total_referrals = 51;
|
||||
});
|
||||
assert_eq!(ReferralPallet::get_referral_score(&REFERRER), 304);
|
||||
|
||||
// 75 referrals: 300 + (25 * 4) = 400
|
||||
ReferrerStatsStorage::<Test>::mutate(&REFERRER, |stats| {
|
||||
stats.total_referrals = 75;
|
||||
});
|
||||
assert_eq!(ReferralPallet::get_referral_score(&REFERRER), 400);
|
||||
|
||||
// 100 referrals: 300 + (50 * 4) = 500
|
||||
ReferrerStatsStorage::<Test>::mutate(&REFERRER, |stats| {
|
||||
stats.total_referrals = 100;
|
||||
});
|
||||
assert_eq!(ReferralPallet::get_referral_score(&REFERRER), 500);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn referral_score_capped_at_500() {
|
||||
use crate::types::ReferralScoreProvider;
|
||||
|
||||
new_test_ext().execute_with(|| {
|
||||
// 101+ referrals capped at 500
|
||||
ReferrerStatsStorage::<Test>::mutate(&REFERRER, |stats| {
|
||||
stats.total_referrals = 101;
|
||||
});
|
||||
assert_eq!(ReferralPallet::get_referral_score(&REFERRER), 500);
|
||||
|
||||
// Even 1000 referrals = 500
|
||||
ReferrerStatsStorage::<Test>::mutate(&REFERRER, |stats| {
|
||||
stats.total_referrals = 1000;
|
||||
});
|
||||
assert_eq!(ReferralPallet::get_referral_score(&REFERRER), 500);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn referral_score_with_balanced_penalty() {
|
||||
use crate::types::ReferralScoreProvider;
|
||||
|
||||
new_test_ext().execute_with(|| {
|
||||
// 10 good referrals = 100 points
|
||||
ReferrerStatsStorage::<Test>::mutate(&REFERRER, |stats| {
|
||||
stats.total_referrals = 10;
|
||||
stats.revoked_referrals = 0;
|
||||
});
|
||||
assert_eq!(ReferralPallet::get_referral_score(&REFERRER), 100);
|
||||
|
||||
// 10 total, 4 revoked = 6 good
|
||||
// Penalty: (4 * 10) / 4 = 10 points deducted
|
||||
// Base score: 6 * 10 = 60
|
||||
// Final: 60 - 10 = 50
|
||||
ReferrerStatsStorage::<Test>::mutate(&REFERRER, |stats| {
|
||||
stats.total_referrals = 10;
|
||||
stats.revoked_referrals = 4;
|
||||
});
|
||||
assert_eq!(ReferralPallet::get_referral_score(&REFERRER), 50);
|
||||
|
||||
// 20 total, 8 revoked = 12 good (tier 2)
|
||||
// Penalty: (8 * 10) / 4 = 20 points deducted
|
||||
// Base score: 100 + (2 * 5) = 110
|
||||
// Final: 110 - 20 = 90
|
||||
ReferrerStatsStorage::<Test>::mutate(&REFERRER, |stats| {
|
||||
stats.total_referrals = 20;
|
||||
stats.revoked_referrals = 8;
|
||||
});
|
||||
assert_eq!(ReferralPallet::get_referral_score(&REFERRER), 90);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn referral_score_cannot_go_negative() {
|
||||
use crate::types::ReferralScoreProvider;
|
||||
|
||||
new_test_ext().execute_with(|| {
|
||||
// Extreme case: All referrals revoked
|
||||
// 5 total, 5 revoked = 0 good
|
||||
// Penalty: (5 * 10) / 4 = 12 points
|
||||
// Base score: 0
|
||||
// Final: 0 - 12 = 0 (saturating_sub)
|
||||
ReferrerStatsStorage::<Test>::mutate(&REFERRER, |stats| {
|
||||
stats.total_referrals = 5;
|
||||
stats.revoked_referrals = 5;
|
||||
});
|
||||
assert_eq!(ReferralPallet::get_referral_score(&REFERRER), 0);
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// InviterProvider Trait Tests
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn get_inviter_returns_correct_referrer() {
|
||||
use crate::types::InviterProvider;
|
||||
|
||||
new_test_ext().execute_with(|| {
|
||||
// Complete referral
|
||||
pezpallet_identity_kyc::KycStatuses::<Test>::insert(
|
||||
REFERRED,
|
||||
pezpallet_identity_kyc::types::KycLevel::Approved,
|
||||
);
|
||||
ReferralPallet::on_kyc_approved(&REFERRED, &REFERRER);
|
||||
|
||||
// Verify InviterProvider trait
|
||||
assert_eq!(ReferralPallet::get_inviter(&REFERRED), Some(REFERRER));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_inviter_returns_none_for_non_referred() {
|
||||
use crate::types::InviterProvider;
|
||||
|
||||
new_test_ext().execute_with(|| {
|
||||
// User was not referred
|
||||
assert_eq!(ReferralPallet::get_inviter(&999), None);
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Force Confirm Referral Tests (Sudo-only)
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn force_confirm_referral_works() {
|
||||
use crate::types::{InviterProvider, ReferralScoreProvider};
|
||||
|
||||
new_test_ext().execute_with(|| {
|
||||
// Force confirm referral (sudo-only)
|
||||
assert_ok!(ReferralPallet::force_confirm_referral(
|
||||
RuntimeOrigin::root(),
|
||||
REFERRER,
|
||||
REFERRED
|
||||
));
|
||||
|
||||
// Verify storage updates
|
||||
assert_eq!(ReferralCount::<Test>::get(REFERRER), 1);
|
||||
assert!(Referrals::<Test>::contains_key(REFERRED));
|
||||
assert_eq!(Referrals::<Test>::get(REFERRED).unwrap().referrer, REFERRER);
|
||||
|
||||
// Verify trait implementations
|
||||
assert_eq!(ReferralPallet::get_inviter(&REFERRED), Some(REFERRER));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn force_confirm_referral_requires_root() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// Non-root origin should fail
|
||||
assert_noop!(
|
||||
ReferralPallet::force_confirm_referral(
|
||||
RuntimeOrigin::signed(REFERRER),
|
||||
REFERRER,
|
||||
REFERRED
|
||||
),
|
||||
DispatchError::BadOrigin
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn force_confirm_referral_prevents_self_referral() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_noop!(
|
||||
ReferralPallet::force_confirm_referral(RuntimeOrigin::root(), REFERRER, REFERRER),
|
||||
Error::<Test>::SelfReferral
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn force_confirm_referral_prevents_duplicate() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// First force confirm succeeds
|
||||
assert_ok!(ReferralPallet::force_confirm_referral(
|
||||
RuntimeOrigin::root(),
|
||||
REFERRER,
|
||||
REFERRED
|
||||
));
|
||||
|
||||
// Second attempt fails
|
||||
assert_noop!(
|
||||
ReferralPallet::force_confirm_referral(RuntimeOrigin::root(), REFERRER, REFERRED),
|
||||
Error::<Test>::AlreadyReferred
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Integration Tests
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn complete_referral_flow_integration() {
|
||||
use crate::types::{InviterProvider, ReferralScoreProvider};
|
||||
|
||||
new_test_ext().execute_with(|| {
|
||||
// Step 1: Initiate referral (legacy way via PendingReferrals)
|
||||
assert_ok!(ReferralPallet::initiate_referral(RuntimeOrigin::signed(REFERRER), REFERRED));
|
||||
assert_eq!(PendingReferrals::<Test>::get(REFERRED), Some(REFERRER));
|
||||
|
||||
// Step 2: KYC approval triggers confirmation
|
||||
pezpallet_identity_kyc::KycStatuses::<Test>::insert(
|
||||
REFERRED,
|
||||
pezpallet_identity_kyc::types::KycLevel::Approved,
|
||||
);
|
||||
ReferralPallet::on_kyc_approved(&REFERRED, &REFERRER);
|
||||
|
||||
// Step 3: Verify all storage updates
|
||||
assert_eq!(PendingReferrals::<Test>::get(REFERRED), None);
|
||||
assert_eq!(ReferralCount::<Test>::get(REFERRER), 1);
|
||||
assert!(Referrals::<Test>::contains_key(REFERRED));
|
||||
|
||||
// Step 4: Verify trait implementations
|
||||
assert_eq!(ReferralPallet::get_inviter(&REFERRED), Some(REFERRER));
|
||||
assert_eq!(ReferralPallet::get_referral_score(&REFERRER), 10);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_referrals_for_same_referrer() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// REFERRER refers 3 people
|
||||
let referred1 = 10;
|
||||
let referred2 = 11;
|
||||
let referred3 = 12;
|
||||
|
||||
// Approve all via direct calls
|
||||
for &referred in &[referred1, referred2, referred3] {
|
||||
pezpallet_identity_kyc::KycStatuses::<Test>::insert(
|
||||
referred,
|
||||
pezpallet_identity_kyc::types::KycLevel::Approved,
|
||||
);
|
||||
ReferralPallet::on_kyc_approved(&referred, &REFERRER);
|
||||
}
|
||||
|
||||
// Verify count
|
||||
assert_eq!(ReferralCount::<Test>::get(REFERRER), 3);
|
||||
|
||||
// Verify stats
|
||||
let stats = ReferrerStatsStorage::<Test>::get(REFERRER);
|
||||
assert_eq!(stats.total_referrals, 3);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn referral_info_stores_block_number() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let block_number = 42u64;
|
||||
System::set_block_number(block_number);
|
||||
|
||||
pezpallet_identity_kyc::KycStatuses::<Test>::insert(
|
||||
REFERRED,
|
||||
pezpallet_identity_kyc::types::KycLevel::Approved,
|
||||
);
|
||||
ReferralPallet::on_kyc_approved(&REFERRED, &REFERRER);
|
||||
|
||||
// Verify stored block number
|
||||
let info = Referrals::<Test>::get(REFERRED).unwrap();
|
||||
assert_eq!(info.created_at, block_number);
|
||||
assert_eq!(info.referrer, REFERRER);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
use codec::{Decode, Encode, MaxEncodedLen};
|
||||
use pezframe_support::pezpallet_prelude::RuntimeDebug;
|
||||
use scale_info::TypeInfo;
|
||||
|
||||
// --- GENERAL TYPES ---
|
||||
|
||||
/// Structure representing a simple NFT.
|
||||
/// Note: The actual NFT structure will be more detailed in `pezpallet-tiki`.
|
||||
#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen, Default)]
|
||||
pub struct Tiki {
|
||||
pub id: u32,
|
||||
// metadata and other fields can be added in the future.
|
||||
}
|
||||
|
||||
/// Raw score type to be used in scoring.
|
||||
pub type RawScore = u32;
|
||||
|
||||
/// Referrer statistics for direct responsibility tracking
|
||||
/// Used to apply penalties when referrals turn out to be malicious
|
||||
#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen, Default)]
|
||||
pub struct ReferrerStats {
|
||||
/// Total number of successful referrals
|
||||
pub total_referrals: u32,
|
||||
/// Number of referrals that were later revoked (bad referrals)
|
||||
pub revoked_referrals: u32,
|
||||
/// Penalty score (affects trust score negatively)
|
||||
/// Formula: revoked_referrals * PenaltyPerRevocation
|
||||
pub penalty_score: u32,
|
||||
}
|
||||
|
||||
impl ReferrerStats {
|
||||
/// Check if referrer has a good track record
|
||||
/// Returns true if less than 10% of referrals were revoked
|
||||
pub fn has_good_track_record(&self) -> bool {
|
||||
if self.total_referrals == 0 {
|
||||
return true;
|
||||
}
|
||||
// Good if less than 10% revoked
|
||||
self.revoked_referrals * 10 < self.total_referrals
|
||||
}
|
||||
|
||||
/// Calculate adjusted referral score with penalty
|
||||
/// Good referrals contribute positively, bad ones contribute negatively
|
||||
pub fn adjusted_score(&self, penalty_per_revocation: u32) -> i32 {
|
||||
let positive = self.total_referrals.saturating_sub(self.revoked_referrals) as i32;
|
||||
let negative = (self.revoked_referrals * penalty_per_revocation) as i32;
|
||||
positive.saturating_sub(negative)
|
||||
}
|
||||
}
|
||||
|
||||
// --- EXTERNAL INTERFACES (TRAITS) ---
|
||||
|
||||
/// Interface for querying an account's inviter.
|
||||
pub trait InviterProvider<AccountId> {
|
||||
fn get_inviter(who: &AccountId) -> Option<AccountId>;
|
||||
}
|
||||
|
||||
/// Interface for calculating an account's referral score.
|
||||
pub trait ReferralScoreProvider<AccountId> {
|
||||
type Score;
|
||||
fn get_referral_score(who: &AccountId) -> Self::Score;
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
|
||||
//! Autogenerated weights for `pezpallet_referral`
|
||||
//!
|
||||
//! THIS FILE WAS AUTO-GENERATED USING THE BIZINIKIWI BENCHMARK CLI VERSION 32.0.0
|
||||
//! DATE: 2025-12-08, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
|
||||
//! WORST CASE MAP SIZE: `1000000`
|
||||
//! HOSTNAME: `MamostePC`, CPU: `11th Gen Intel(R) Core(TM) i9-11950H @ 2.60GHz`
|
||||
//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: `1024`
|
||||
|
||||
// Executed Command:
|
||||
// ./target/release/frame-omni-bencher
|
||||
// v1
|
||||
// benchmark
|
||||
// pallet
|
||||
// --runtime
|
||||
// target/release/wbuild/people-pezkuwichain-runtime/people_pezkuwichain_runtime.compact.compressed.wasm
|
||||
// --pallets
|
||||
// pezpallet_referral
|
||||
// -e
|
||||
// all
|
||||
// --steps
|
||||
// 50
|
||||
// --repeat
|
||||
// 20
|
||||
// --output
|
||||
// pezcumulus/teyrchains/pallets/referral/src/weights.rs
|
||||
// --template
|
||||
// bizinikiwi/.maintain/frame-weight-template.hbs
|
||||
|
||||
#![cfg_attr(rustfmt, rustfmt_skip)]
|
||||
#![allow(unused_parens)]
|
||||
#![allow(unused_imports)]
|
||||
#![allow(missing_docs)]
|
||||
#![allow(dead_code)]
|
||||
|
||||
use pezframe_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}};
|
||||
use core::marker::PhantomData;
|
||||
|
||||
/// Weight functions needed for `pezpallet_referral`.
|
||||
pub trait WeightInfo {
|
||||
fn initiate_referral() -> Weight;
|
||||
fn force_confirm_referral() -> Weight;
|
||||
}
|
||||
|
||||
/// Weights for `pezpallet_referral` using the Bizinikiwi node and recommended hardware.
|
||||
pub struct BizinikiwiWeight<T>(PhantomData<T>);
|
||||
impl<T: pezframe_system::Config> WeightInfo for BizinikiwiWeight<T> {
|
||||
/// Storage: `Referral::Referrals` (r:1 w:0)
|
||||
/// Proof: `Referral::Referrals` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Referral::PendingReferrals` (r:1 w:1)
|
||||
/// Proof: `Referral::PendingReferrals` (`max_values`: None, `max_size`: Some(80), added: 2555, mode: `MaxEncodedLen`)
|
||||
fn initiate_referral() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `3`
|
||||
// Estimated: `3549`
|
||||
// Minimum execution time: 11_365_000 picoseconds.
|
||||
Weight::from_parts(11_696_000, 3549)
|
||||
.saturating_add(T::DbWeight::get().reads(2_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(1_u64))
|
||||
}
|
||||
/// Storage: `Referral::Referrals` (r:1 w:1)
|
||||
/// Proof: `Referral::Referrals` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Referral::ReferralCount` (r:1 w:1)
|
||||
/// Proof: `Referral::ReferralCount` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Referral::PendingReferrals` (r:0 w:1)
|
||||
/// Proof: `Referral::PendingReferrals` (`max_values`: None, `max_size`: Some(80), added: 2555, mode: `MaxEncodedLen`)
|
||||
fn force_confirm_referral() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `3`
|
||||
// Estimated: `3549`
|
||||
// Minimum execution time: 14_792_000 picoseconds.
|
||||
Weight::from_parts(15_141_000, 3549)
|
||||
.saturating_add(T::DbWeight::get().reads(2_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(3_u64))
|
||||
}
|
||||
}
|
||||
|
||||
// For backwards compatibility and tests.
|
||||
impl WeightInfo for () {
|
||||
/// Storage: `Referral::Referrals` (r:1 w:0)
|
||||
/// Proof: `Referral::Referrals` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Referral::PendingReferrals` (r:1 w:1)
|
||||
/// Proof: `Referral::PendingReferrals` (`max_values`: None, `max_size`: Some(80), added: 2555, mode: `MaxEncodedLen`)
|
||||
fn initiate_referral() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `3`
|
||||
// Estimated: `3549`
|
||||
// Minimum execution time: 11_365_000 picoseconds.
|
||||
Weight::from_parts(11_696_000, 3549)
|
||||
.saturating_add(RocksDbWeight::get().reads(2_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(1_u64))
|
||||
}
|
||||
/// Storage: `Referral::Referrals` (r:1 w:1)
|
||||
/// Proof: `Referral::Referrals` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Referral::ReferralCount` (r:1 w:1)
|
||||
/// Proof: `Referral::ReferralCount` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Referral::PendingReferrals` (r:0 w:1)
|
||||
/// Proof: `Referral::PendingReferrals` (`max_values`: None, `max_size`: Some(80), added: 2555, mode: `MaxEncodedLen`)
|
||||
fn force_confirm_referral() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `3`
|
||||
// Estimated: `3549`
|
||||
// Minimum execution time: 14_792_000 picoseconds.
|
||||
Weight::from_parts(15_141_000, 3549)
|
||||
.saturating_add(RocksDbWeight::get().reads(2_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(3_u64))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
[package]
|
||||
name = "pezpallet-staking-score"
|
||||
version = "1.0.0"
|
||||
description = "PezkuwiChain Staking Score Calculation Pallet"
|
||||
authors.workspace = true
|
||||
homepage.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
publish = false
|
||||
repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
codec = { workspace = true, default-features = false, features = ["derive"] }
|
||||
pezframe-benchmarking = { workspace = true, optional = true }
|
||||
pezframe-support = { default-features = false, workspace = true }
|
||||
pezframe-system = { default-features = false, workspace = true }
|
||||
pezpallet-balances = { workspace = true, default-features = false, optional = true }
|
||||
pezpallet-staking = { workspace = true, default-features = false, optional = true }
|
||||
scale-info = { default-features = false, features = [
|
||||
"derive",
|
||||
], workspace = true }
|
||||
serde = { version = "1.0.197", default-features = false, features = [
|
||||
"derive",
|
||||
], optional = true }
|
||||
pezsp-runtime = { default-features = false, workspace = true }
|
||||
pezsp-std = { default-features = false, workspace = true }
|
||||
|
||||
# PezkuwiChain'in özel tiplerini ve trait'lerini içeren kütüphane
|
||||
pezkuwi-primitives = { workspace = true, default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
pezframe-election-provider-support = { workspace = true, features = ["std"] }
|
||||
pezframe-support = { workspace = true, features = ["std"] }
|
||||
pezframe-system = { workspace = true, features = ["std"] }
|
||||
pezpallet-bags-list = { workspace = true, features = ["std"] }
|
||||
pezpallet-balances = { workspace = true, features = ["std"] }
|
||||
pezpallet-session = { workspace = true, features = ["std"] }
|
||||
pezpallet-staking = { workspace = true, features = ["runtime-benchmarks", "std"] }
|
||||
pezpallet-timestamp = { workspace = true, features = ["std"] }
|
||||
pezsp-core = { workspace = true, features = ["std"] }
|
||||
pezsp-io = { workspace = true, features = ["std"] }
|
||||
pezsp-npos-elections = { workspace = true, features = ["std"] }
|
||||
pezsp-runtime = { workspace = true, features = ["std"] }
|
||||
pezsp-staking = { workspace = true, features = ["std"] }
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = [
|
||||
"codec/std",
|
||||
"pezframe-benchmarking?/std",
|
||||
"pezframe-election-provider-support/std",
|
||||
"pezframe-support/std",
|
||||
"pezframe-system/std",
|
||||
"pezpallet-bags-list/std",
|
||||
"pezpallet-balances?/std",
|
||||
"pezpallet-session/std",
|
||||
"pezpallet-staking?/std",
|
||||
"pezpallet-timestamp/std",
|
||||
"pezkuwi-primitives/std",
|
||||
"scale-info/std",
|
||||
"serde?/std",
|
||||
"pezsp-core/std",
|
||||
"pezsp-io/std",
|
||||
"pezsp-npos-elections/std",
|
||||
"pezsp-runtime/std",
|
||||
"pezsp-staking/std",
|
||||
"pezsp-std/std",
|
||||
]
|
||||
runtime-benchmarks = [
|
||||
"pezframe-benchmarking/runtime-benchmarks",
|
||||
"pezframe-election-provider-support/runtime-benchmarks",
|
||||
"pezframe-support/runtime-benchmarks",
|
||||
"pezframe-system/runtime-benchmarks",
|
||||
"pezpallet-bags-list/runtime-benchmarks",
|
||||
"pezpallet-balances/runtime-benchmarks",
|
||||
"pezpallet-session/runtime-benchmarks",
|
||||
"pezpallet-staking/runtime-benchmarks",
|
||||
"pezpallet-timestamp/runtime-benchmarks",
|
||||
"pezkuwi-primitives/runtime-benchmarks",
|
||||
"pezsp-io/runtime-benchmarks",
|
||||
"pezsp-npos-elections/runtime-benchmarks",
|
||||
"pezsp-runtime/runtime-benchmarks",
|
||||
"pezsp-staking/runtime-benchmarks",
|
||||
]
|
||||
try-runtime = [
|
||||
"pezframe-election-provider-support/try-runtime",
|
||||
"pezframe-support/try-runtime",
|
||||
"pezframe-system/try-runtime",
|
||||
"pezpallet-bags-list/try-runtime",
|
||||
"pezpallet-balances?/try-runtime",
|
||||
"pezpallet-session/try-runtime",
|
||||
"pezpallet-staking?/try-runtime",
|
||||
"pezpallet-timestamp/try-runtime",
|
||||
"pezsp-runtime/try-runtime",
|
||||
]
|
||||
@@ -0,0 +1,35 @@
|
||||
//! Benchmarking setup for pezpallet-staking-score
|
||||
|
||||
use super::*;
|
||||
use crate::{Config, Pallet, StakingStartBlock};
|
||||
use pezframe_benchmarking::v2::*;
|
||||
use pezframe_system::RawOrigin;
|
||||
|
||||
#[benchmarks]
|
||||
mod benchmarks {
|
||||
use super::*;
|
||||
|
||||
#[benchmark]
|
||||
fn start_score_tracking() {
|
||||
let caller: T::AccountId = whitelisted_caller();
|
||||
|
||||
// Mock staking provider kullanıyoruz, gerçek staking setup'ı yapmıyoruz
|
||||
// Runtime'da conditional olarak MockStakingInfoProvider kullanılacak
|
||||
|
||||
// Ölçümden önce, bu kullanıcının daha önce takibi başlatmadığından emin olalım.
|
||||
StakingStartBlock::<T>::remove(&caller);
|
||||
|
||||
// EYLEM: Bu bloğun içindeki extrinsic çağrısının ne kadar sürdüğünü ölçüyoruz.
|
||||
#[extrinsic_call]
|
||||
_(RawOrigin::Signed(caller.clone()));
|
||||
|
||||
// DOĞRULAMA: Mock provider kullanıldığında bu başarılı olmalı
|
||||
assert!(StakingStartBlock::<T>::get(&caller).is_some());
|
||||
}
|
||||
|
||||
impl_benchmark_test_suite!(
|
||||
StakingScore,
|
||||
crate::mock::ExtBuilder::default().build(),
|
||||
crate::mock::Test,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
//! # Staking Score Pallet
|
||||
//!
|
||||
//! A pallet for calculating time-weighted staking scores based on stake amount and duration.
|
||||
//!
|
||||
//! ## Overview
|
||||
//!
|
||||
//! The Staking Score pallet calculates reputation scores from staking behavior by considering:
|
||||
//! - **Stake Amount**: How much a user has staked
|
||||
//! - **Stake Duration**: How long tokens have been staked
|
||||
//! - **Nomination Count**: Number of validators nominated
|
||||
//! - **Unlocking Chunks**: Pending unstake operations
|
||||
//!
|
||||
//! These metrics combine to produce a staking score that contributes to the composite
|
||||
//! trust score in `pezpallet-trust`.
|
||||
//!
|
||||
//! ## Score Calculation
|
||||
//!
|
||||
//! ```text
|
||||
//! staking_score = base_score + time_bonus
|
||||
//!
|
||||
//! where:
|
||||
//! base_score = (staked_amount / UNITS) * 10
|
||||
//! time_bonus = (months_staked * staked_amount * 0.05) / UNITS
|
||||
//! ```
|
||||
//!
|
||||
//! ### Time-Based Rewards
|
||||
//! - First month: Base score only
|
||||
//! - Each additional month: +5% bonus on staked amount
|
||||
//! - Maximum benefit achieved through long-term commitment
|
||||
//! - Score increases linearly with time
|
||||
//!
|
||||
//! ## Workflow
|
||||
//!
|
||||
//! 1. User stakes tokens via main staking pallet
|
||||
//! 2. User calls `start_score_tracking()` to begin time tracking
|
||||
//! 3. Tracking start block is recorded
|
||||
//! 4. `pezpallet-trust` queries staking score via `StakingScoreProvider` trait
|
||||
//! 5. Score calculation uses current block number vs. start block
|
||||
//! 6. Time bonus accumulates automatically each month
|
||||
//!
|
||||
//! ## Integration with Staking
|
||||
//!
|
||||
//! This pallet does not handle staking operations directly. It:
|
||||
//! - Reads staking data from main staking pallet via `StakingInfoProvider`
|
||||
//! - Tracks when users want to start earning time bonuses
|
||||
//! - Calculates scores on-demand without modifying staking state
|
||||
//!
|
||||
//! ## Score Components
|
||||
//!
|
||||
//! ### Staked Amount
|
||||
//! - Primary factor in score calculation
|
||||
//! - Measured in balance units (UNITS = 10^12)
|
||||
//! - Higher stake = higher base score
|
||||
//!
|
||||
//! ### Duration
|
||||
//! - Measured in months (30 days * 24 hours * 60 min * 10 blocks/min)
|
||||
//! - ~432,000 blocks per month
|
||||
//! - Compounds monthly for long-term stakers
|
||||
//!
|
||||
//! ### Additional Metrics
|
||||
//! - Nomination count (contributes to complexity score)
|
||||
//! - Unlocking chunks (indicates unstaking activity)
|
||||
//!
|
||||
//! ## Interface
|
||||
//!
|
||||
//! ### Extrinsics
|
||||
//!
|
||||
//! - `start_score_tracking()` - Begin time-based score accumulation (user, one-time)
|
||||
//!
|
||||
//! ### Storage
|
||||
//!
|
||||
//! - `StakingStartBlock` - Block number when user started score tracking
|
||||
//!
|
||||
//! ### Trait Implementations
|
||||
//!
|
||||
//! - `StakingScoreProvider` - Query staking scores for trust calculation
|
||||
//!
|
||||
//! ## Dependencies
|
||||
//!
|
||||
//! This pallet requires:
|
||||
//! - Main staking pallet implementing `StakingInfoProvider`
|
||||
//! - `pezpallet-trust` as consumer of staking scores
|
||||
//!
|
||||
//! ## Runtime Integration Example
|
||||
//!
|
||||
//! ```ignore
|
||||
//! impl pezpallet_staking_score::Config for Runtime {
|
||||
//! type RuntimeEvent = RuntimeEvent;
|
||||
//! type Balance = Balance;
|
||||
//! type StakingInfo = Staking; // Main staking pallet
|
||||
//! type WeightInfo = pezpallet_staking_score::weights::BizinikiwiWeight<Runtime>;
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
pub use pallet::*;
|
||||
|
||||
// Mock staking info provider for benchmarking - ADD THIS
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
pub struct BenchmarkStakingInfoProvider;
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
impl<AccountId, Balance> StakingInfoProvider<AccountId, Balance> for BenchmarkStakingInfoProvider
|
||||
where
|
||||
Balance: From<u128>,
|
||||
{
|
||||
fn get_staking_details(_who: &AccountId) -> Option<StakingDetails<Balance>> {
|
||||
// Always return valid stake for benchmarking
|
||||
Some(StakingDetails {
|
||||
staked_amount: (1000u128 * UNITS).into(),
|
||||
nominations_count: 5,
|
||||
unlocking_chunks_count: 2,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
pub mod benchmarking;
|
||||
#[cfg(test)]
|
||||
mod mock;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub mod weights;
|
||||
|
||||
#[pezframe_support::pallet]
|
||||
pub mod pallet {
|
||||
use super::weights::WeightInfo; // Properly importing WeightInfo from parent module.
|
||||
use core::ops::Div;
|
||||
use pezframe_support::pezpallet_prelude::*;
|
||||
use pezframe_system::pezpallet_prelude::*;
|
||||
use pezsp_runtime::{
|
||||
traits::{Saturating, Zero},
|
||||
Perbill,
|
||||
};
|
||||
|
||||
// --- Sabitler ---
|
||||
pub const MONTH_IN_BLOCKS: u32 = 30 * 24 * 60 * 10;
|
||||
pub const UNITS: u128 = 1_000_000_000_000;
|
||||
|
||||
#[pallet::pallet]
|
||||
pub struct Pallet<T>(_);
|
||||
|
||||
#[pallet::config]
|
||||
pub trait Config: pezframe_system::Config
|
||||
where
|
||||
// Ensuring BlockNumber is convertible from u32.
|
||||
BlockNumberFor<Self>: From<u32>,
|
||||
{
|
||||
type RuntimeEvent: From<Event<Self>> + IsType<<Self as pezframe_system::Config>::RuntimeEvent>;
|
||||
/// Balance type to be used for staking.
|
||||
/// Adding all required mathematical and comparison properties.
|
||||
type Balance: Member
|
||||
+ Parameter
|
||||
+ MaxEncodedLen
|
||||
+ Copy
|
||||
+ Default
|
||||
+ PartialOrd
|
||||
+ Saturating
|
||||
+ Zero
|
||||
+ Div<Output = Self::Balance> // Specifying that division result is also Balance.
|
||||
+ From<u128>;
|
||||
/// Interface to be used for reading staking data.
|
||||
type StakingInfo: StakingInfoProvider<Self::AccountId, Self::Balance>;
|
||||
/// To provide extrinsic weights.
|
||||
type WeightInfo: WeightInfo;
|
||||
}
|
||||
|
||||
// --- Depolama (Storage) ---
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn staking_start_block)]
|
||||
pub type StakingStartBlock<T: Config> =
|
||||
StorageMap<_, Blake2_128Concat, T::AccountId, BlockNumberFor<T>, OptionQuery>;
|
||||
|
||||
#[pallet::event]
|
||||
#[pallet::generate_deposit(pub(super) fn deposit_event)]
|
||||
pub enum Event<T: Config> {
|
||||
/// A user started time-based scoring.
|
||||
ScoreTrackingStarted { who: T::AccountId, start_block: BlockNumberFor<T> },
|
||||
}
|
||||
|
||||
#[pallet::error]
|
||||
pub enum Error<T> {
|
||||
/// Puan takibini başlatmak için önce stake yapmış olmalısınız.
|
||||
NoStakeFound,
|
||||
/// Puan takibi zaten daha önce başlatılmış.
|
||||
TrackingAlreadyStarted,
|
||||
}
|
||||
|
||||
#[pallet::call]
|
||||
impl<T: Config> Pallet<T> {
|
||||
/// Süreye dayalı puanlamayı manuel olarak aktive eder.
|
||||
/// Bu fonksiyon, her kullanıcı tarafından sadece bir kez çağrılmalıdır.
|
||||
#[pallet::call_index(0)]
|
||||
#[pallet::weight(T::WeightInfo::start_score_tracking())]
|
||||
pub fn start_score_tracking(origin: OriginFor<T>) -> DispatchResult {
|
||||
let who = ensure_signed(origin)?;
|
||||
|
||||
// 1. Kullanıcının puan takibini daha önce başlatıp başlatmadığını kontrol et.
|
||||
ensure!(
|
||||
StakingStartBlock::<T>::get(&who).is_none(),
|
||||
Error::<T>::TrackingAlreadyStarted
|
||||
);
|
||||
|
||||
// 2. Kullanıcının ana staking paletinde stake'i var mı diye kontrol et.
|
||||
// `get_staking_details` artık Option döndürdüğü için `ok_or` ile hata yönetimi
|
||||
// yapıyoruz.
|
||||
let details =
|
||||
T::StakingInfo::get_staking_details(&who).ok_or(Error::<T>::NoStakeFound)?;
|
||||
ensure!(!details.staked_amount.is_zero(), Error::<T>::NoStakeFound);
|
||||
|
||||
// 3. O anki blok numarasını kaydet.
|
||||
let current_block = pezframe_system::Pallet::<T>::block_number();
|
||||
StakingStartBlock::<T>::insert(&who, current_block);
|
||||
|
||||
Self::deposit_event(Event::ScoreTrackingStarted { who, start_block: current_block });
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// --- Arayüz (Trait) ve Tip Tanımları ---
|
||||
|
||||
/// Puanlamada kullanılacak ham skor tipi.
|
||||
pub type RawScore = u32;
|
||||
|
||||
/// Staking ile ilgili detayları bir arada tutan ve dışarıdan alınacak veri yapısı.
|
||||
/// `Default` ekledik çünkü testlerde ve mock'larda işimizi kolaylaştıracak.
|
||||
#[derive(Default, Encode, Decode, Clone, PartialEq, Eq, TypeInfo, Debug)]
|
||||
pub struct StakingDetails<Balance> {
|
||||
pub staked_amount: Balance,
|
||||
pub nominations_count: u32,
|
||||
pub unlocking_chunks_count: u32,
|
||||
}
|
||||
|
||||
/// Bu paletin dış dünyaya sunduğu arayüz.
|
||||
pub trait StakingScoreProvider<AccountId, BlockNumber> {
|
||||
/// Returns the score and the duration in blocks used for calculation.
|
||||
fn get_staking_score(who: &AccountId) -> (RawScore, BlockNumber);
|
||||
}
|
||||
|
||||
/// Bu paletin, staking verilerini almak için ihtiyaç duyduğu arayüz.
|
||||
pub trait StakingInfoProvider<AccountId, Balance> {
|
||||
/// Verilen hesap için staking detaylarını döndürür.
|
||||
/// Eğer kullanıcının stake'i yoksa `None` dönmelidir. Bu daha güvenli bir yöntemdir.
|
||||
fn get_staking_details(who: &AccountId) -> Option<StakingDetails<Balance>>;
|
||||
}
|
||||
|
||||
// --- Trait Implementasyonu ---
|
||||
|
||||
impl<T: Config> StakingScoreProvider<T::AccountId, BlockNumberFor<T>> for Pallet<T> {
|
||||
fn get_staking_score(who: &T::AccountId) -> (RawScore, BlockNumberFor<T>) {
|
||||
// 1. Staking detaylarını al. Eğer stake yoksa (None) 0 puan döndür.
|
||||
let staking_details = match T::StakingInfo::get_staking_details(who) {
|
||||
Some(details) => details,
|
||||
None => return (0, Zero::zero()),
|
||||
};
|
||||
|
||||
// Staked miktarı ana birime (HEZ) çevir.
|
||||
let staked_hez: T::Balance = staking_details.staked_amount / UNITS.into();
|
||||
|
||||
// "Sıfır stake, sıfır puan" kuralını uygula.
|
||||
if staked_hez.is_zero() {
|
||||
return (0, Zero::zero());
|
||||
}
|
||||
|
||||
// Miktara dayalı temel puanı hesapla.
|
||||
let amount_score: u32 = if staked_hez <= 100u128.into() {
|
||||
20
|
||||
} else if staked_hez <= 250u128.into() {
|
||||
30
|
||||
} else if staked_hez <= 750u128.into() {
|
||||
40
|
||||
} else {
|
||||
50 // 751+ HEZ
|
||||
};
|
||||
|
||||
// Süreye dayalı çarpanı ve duration'ı hesapla.
|
||||
let (_duration_multiplier, duration_for_return) = match StakingStartBlock::<T>::get(who)
|
||||
{
|
||||
// Eğer kullanıcı `start_score_tracking` çağırdıysa...
|
||||
Some(start_block) => {
|
||||
let current_block = pezframe_system::Pallet::<T>::block_number();
|
||||
let duration_in_blocks = current_block.saturating_sub(start_block);
|
||||
|
||||
let multiplier = if duration_in_blocks >= (12 * MONTH_IN_BLOCKS).into() {
|
||||
Perbill::from_rational(2u32, 1u32) // x2.0 (12 ay ve üstü)
|
||||
} else if duration_in_blocks >= (6 * MONTH_IN_BLOCKS).into() {
|
||||
Perbill::from_rational(17u32, 10u32) // x1.7 (6-11 ay)
|
||||
} else if duration_in_blocks >= (3 * MONTH_IN_BLOCKS).into() {
|
||||
Perbill::from_rational(7u32, 5u32) // x1.4 (3-5 ay)
|
||||
} else if duration_in_blocks >= MONTH_IN_BLOCKS.into() {
|
||||
Perbill::from_rational(6u32, 5u32) // x1.2 (1-2 ay)
|
||||
} else {
|
||||
Perbill::from_rational(1u32, 1u32) // x1.0 (< 1 ay)
|
||||
};
|
||||
|
||||
(multiplier, duration_in_blocks)
|
||||
},
|
||||
// Eğer takip başlatılmadıysa, çarpan 1.0'dır.
|
||||
None => (Perbill::from_rational(10u32, 10u32), Zero::zero()),
|
||||
};
|
||||
|
||||
// Nihai puanı hesapla ve 100 ile sınırla.
|
||||
let final_score = match StakingStartBlock::<T>::get(who) {
|
||||
Some(start_block) => {
|
||||
let current_block = pezframe_system::Pallet::<T>::block_number();
|
||||
let duration_in_blocks = current_block.saturating_sub(start_block);
|
||||
|
||||
if duration_in_blocks >= (12 * MONTH_IN_BLOCKS).into() {
|
||||
amount_score * 2 // x2.0
|
||||
} else if duration_in_blocks >= (6 * MONTH_IN_BLOCKS).into() {
|
||||
amount_score * 17 / 10 // x1.7
|
||||
} else if duration_in_blocks >= (3 * MONTH_IN_BLOCKS).into() {
|
||||
amount_score * 14 / 10 // x1.4
|
||||
} else if duration_in_blocks >= MONTH_IN_BLOCKS.into() {
|
||||
amount_score * 12 / 10 // x1.2
|
||||
} else {
|
||||
amount_score // x1.0
|
||||
}
|
||||
},
|
||||
None => amount_score, // Takip başlatılmadıysa çarpan yok
|
||||
};
|
||||
|
||||
(final_score.min(100), duration_for_return)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
//! pezpallet-staking-score için mock runtime.
|
||||
|
||||
use crate as pezpallet_staking_score;
|
||||
use pezframe_support::{
|
||||
construct_runtime, derive_impl, parameter_types,
|
||||
traits::{ConstU128, ConstU32, ConstU64, Everything, Hooks},
|
||||
weights::constants::RocksDbWeight,
|
||||
};
|
||||
use pezframe_system::EnsureRoot;
|
||||
use pezsp_core::H256;
|
||||
use pezsp_runtime::{
|
||||
traits::{BlakeTwo256, IdentityLookup},
|
||||
BuildStorage,
|
||||
};
|
||||
use pezsp_staking::{StakerStatus, StakingAccount};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
// Paletimizdeki sabitleri import ediyoruz.
|
||||
use crate::{MONTH_IN_BLOCKS, UNITS};
|
||||
|
||||
// --- Tip Takma Adları ---
|
||||
type Block = pezframe_system::mocking::MockBlock<Test>;
|
||||
pub type AccountId = u64;
|
||||
pub type Balance = u128;
|
||||
pub type BlockNumber = u64;
|
||||
pub type Nonce = u64;
|
||||
pub type SessionIndex = u32;
|
||||
pub type EraIndex = u32;
|
||||
|
||||
// --- Paletler için Sabitler ---
|
||||
pub const MAX_NOMINATIONS_CONST: u32 = 16;
|
||||
parameter_types! {
|
||||
pub const BlockHashCount: BlockNumber = 250;
|
||||
pub const ExistentialDeposit: Balance = 1;
|
||||
pub static SessionsPerEra: SessionIndex = 3;
|
||||
pub const BondingDuration: u32 = 3;
|
||||
pub const SlashDeferDuration: EraIndex = 0;
|
||||
pub static HistoryDepth: u32 = 80;
|
||||
pub const MaxUnlockingChunks: u32 = 32;
|
||||
pub static MaxNominations: u32 = 16;
|
||||
pub const MinimumPeriod: u64 = 5000;
|
||||
pub static BagThresholds: &'static [u64] = &[10, 20, 30, 40, 50, 60, 1_000, 2_000, 10_000];
|
||||
pub static MaxWinners: u32 = 100;
|
||||
pub static MaxBackersPerWinner: u32 = 64;
|
||||
// Yeni eklenenler: pezpallet_staking::Config için gerekli minimum bond miktarları.
|
||||
pub const MinNominatorBond: Balance = 1 * UNITS; // Testler için yeterince küçük bir değer.
|
||||
pub const MinValidatorBond: Balance = 1 * UNITS; // Testler için yeterince küçük bir değer.
|
||||
}
|
||||
|
||||
// --- construct_runtime! Makrosu ---
|
||||
construct_runtime!(
|
||||
pub enum Test
|
||||
{
|
||||
System: pezframe_system,
|
||||
Balances: pezpallet_balances,
|
||||
Staking: pezpallet_staking,
|
||||
Session: pezpallet_session,
|
||||
Timestamp: pezpallet_timestamp,
|
||||
Historical: pezpallet_session::historical,
|
||||
BagsList: pezpallet_bags_list::<Instance1>,
|
||||
// Kendi paletimiz:
|
||||
StakingScore: pezpallet_staking_score,
|
||||
}
|
||||
);
|
||||
|
||||
#[derive_impl(pezframe_system::config_preludes::TestDefaultConfig)]
|
||||
impl pezframe_system::Config for Test {
|
||||
type DbWeight = RocksDbWeight;
|
||||
type Block = Block;
|
||||
type AccountData = pezpallet_balances::AccountData<Balance>;
|
||||
}
|
||||
|
||||
#[derive_impl(pezpallet_balances::config_preludes::TestDefaultConfig)]
|
||||
impl pezpallet_balances::Config for Test {
|
||||
type MaxLocks = ConstU32<1024>;
|
||||
type Balance = Balance;
|
||||
type ExistentialDeposit = ExistentialDeposit;
|
||||
type AccountStore = System;
|
||||
}
|
||||
|
||||
pezsp_runtime::impl_opaque_keys! {
|
||||
pub struct MockSessionKeys {
|
||||
pub dummy: pezsp_runtime::testing::UintAuthorityId,
|
||||
}
|
||||
}
|
||||
|
||||
impl From<pezsp_runtime::testing::UintAuthorityId> for MockSessionKeys {
|
||||
fn from(dummy: pezsp_runtime::testing::UintAuthorityId) -> Self {
|
||||
Self { dummy }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TestSessionHandler;
|
||||
impl pezpallet_session::SessionHandler<AccountId> for TestSessionHandler {
|
||||
const KEY_TYPE_IDS: &'static [pezsp_runtime::KeyTypeId] = &[pezsp_runtime::key_types::DUMMY];
|
||||
fn on_genesis_session<T: pezsp_runtime::traits::OpaqueKeys>(_validators: &[(AccountId, T)]) {}
|
||||
fn on_new_session<T: pezsp_runtime::traits::OpaqueKeys>(
|
||||
_changed: bool,
|
||||
_validators: &[(AccountId, T)],
|
||||
_queued_validators: &[(AccountId, T)],
|
||||
) {
|
||||
}
|
||||
fn on_before_session_ending() {}
|
||||
fn on_disabled(_validator_index: u32) {}
|
||||
}
|
||||
|
||||
impl pezpallet_session::Config for Test {
|
||||
type SessionManager = pezpallet_session::historical::NoteHistoricalRoot<Test, Staking>;
|
||||
type Keys = MockSessionKeys;
|
||||
type ShouldEndSession = pezpallet_session::PeriodicSessions<SessionsPerEra, ConstU64<0>>;
|
||||
type SessionHandler = TestSessionHandler;
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type ValidatorId = AccountId;
|
||||
type ValidatorIdOf = pezsp_runtime::traits::ConvertInto;
|
||||
type NextSessionRotation = pezpallet_session::PeriodicSessions<SessionsPerEra, ConstU64<0>>;
|
||||
type DisablingStrategy = ();
|
||||
type WeightInfo = ();
|
||||
type Currency = Balances;
|
||||
type KeyDeposit = ConstU128<0>;
|
||||
}
|
||||
|
||||
impl pezpallet_session::historical::Config for Test {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type FullIdentification = pezpallet_staking::Exposure<AccountId, Balance>;
|
||||
type FullIdentificationOf = pezpallet_staking::ExposureOf<Test>;
|
||||
}
|
||||
|
||||
impl pezpallet_timestamp::Config for Test {
|
||||
type Moment = u64;
|
||||
type OnTimestampSet = ();
|
||||
type MinimumPeriod = ConstU64<5>;
|
||||
type WeightInfo = ();
|
||||
}
|
||||
|
||||
type VoterBagsListInstance = pezpallet_bags_list::Instance1;
|
||||
impl pezpallet_bags_list::Config<VoterBagsListInstance> for Test {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type WeightInfo = ();
|
||||
type ScoreProvider = Staking;
|
||||
type BagThresholds = BagThresholds;
|
||||
type Score = pezsp_npos_elections::VoteWeight;
|
||||
type MaxAutoRebagPerBlock = ();
|
||||
}
|
||||
|
||||
pub struct TestBenchmarkingConfig;
|
||||
impl pezpallet_staking::BenchmarkingConfig for TestBenchmarkingConfig {
|
||||
type MaxValidators = ConstU32<1000>;
|
||||
type MaxNominators = ConstU32<1000>;
|
||||
}
|
||||
|
||||
#[derive_impl(pezpallet_staking::config_preludes::TestDefaultConfig)]
|
||||
impl pezpallet_staking::Config for Test {
|
||||
type Currency = Balances;
|
||||
type UnixTime = Timestamp;
|
||||
type SessionsPerEra = SessionsPerEra;
|
||||
type BondingDuration = BondingDuration;
|
||||
type SlashDeferDuration = SlashDeferDuration;
|
||||
type SessionInterface = Self;
|
||||
type EraPayout = ();
|
||||
type NextNewSession = Session;
|
||||
type MaxExposurePageSize = ConstU32<64>;
|
||||
type ElectionProvider = pezframe_election_provider_support::NoElection<(
|
||||
AccountId,
|
||||
BlockNumber,
|
||||
Staking,
|
||||
MaxWinners,
|
||||
MaxBackersPerWinner,
|
||||
)>;
|
||||
type GenesisElectionProvider = Self::ElectionProvider;
|
||||
type VoterList = BagsList;
|
||||
type TargetList = pezpallet_staking::UseValidatorsMap<Self>;
|
||||
type MaxControllersInDeprecationBatch = ConstU32<100>;
|
||||
type AdminOrigin = EnsureRoot<AccountId>;
|
||||
type EventListeners = ();
|
||||
type HistoryDepth = HistoryDepth;
|
||||
type NominationsQuota = pezpallet_staking::FixedNominationsQuota<MAX_NOMINATIONS_CONST>;
|
||||
type MaxUnlockingChunks = MaxUnlockingChunks;
|
||||
type BenchmarkingConfig = TestBenchmarkingConfig;
|
||||
type OldCurrency = Balances;
|
||||
}
|
||||
|
||||
// --- Bizim Paletimiz ve Adaptörü ---
|
||||
pub struct StakingDataProvider;
|
||||
impl crate::StakingInfoProvider<AccountId, Balance> for StakingDataProvider {
|
||||
fn get_staking_details(who: &AccountId) -> Option<crate::StakingDetails<Balance>> {
|
||||
if let Ok(ledger) = Staking::ledger(StakingAccount::Stash(who.clone())) {
|
||||
let nominations_count = Staking::nominators(who).map_or(0, |n| n.targets.len() as u32);
|
||||
let unlocking_chunks_count = ledger.unlocking.len() as u32;
|
||||
|
||||
Some(crate::StakingDetails {
|
||||
staked_amount: ledger.total,
|
||||
nominations_count,
|
||||
unlocking_chunks_count,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::Config for Test {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type Balance = Balance;
|
||||
type WeightInfo = ();
|
||||
type StakingInfo = StakingDataProvider;
|
||||
}
|
||||
|
||||
// --- ExtBuilder ve Yardımcı Fonksiyonlar ---
|
||||
pub struct ExtBuilder {
|
||||
stakers: Vec<(AccountId, AccountId, Balance, StakerStatus<AccountId>)>,
|
||||
}
|
||||
|
||||
impl Default for ExtBuilder {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
// Benchmarking ve testlerin düzgün çalışması için başlangıç staker'larını
|
||||
// testlerde kullanılacak USER_STASH (10) hesabını içermeyecek şekilde ayarlıyoruz.
|
||||
// USER_STASH testlerde manuel olarak bond edilecek.
|
||||
stakers: vec![
|
||||
// Sadece benchmarking için yeterli sayıda validator ve nominator
|
||||
(1, 1, 1_000 * UNITS, StakerStatus::Validator),
|
||||
(2, 2, 1_000 * UNITS, StakerStatus::Validator),
|
||||
(3, 3, 1_000 * UNITS, StakerStatus::Validator),
|
||||
(4, 4, 1_000 * UNITS, StakerStatus::Validator),
|
||||
(5, 5, 1_000 * UNITS, StakerStatus::Validator),
|
||||
(6, 6, 1_000 * UNITS, StakerStatus::Validator),
|
||||
(7, 7, 1_000 * UNITS, StakerStatus::Validator),
|
||||
(8, 8, 1_000 * UNITS, StakerStatus::Validator),
|
||||
(9, 9, 1_000 * UNITS, StakerStatus::Validator),
|
||||
(11, 11, 100 * UNITS, StakerStatus::Nominator(vec![1, 2])),
|
||||
(12, 12, 100 * UNITS, StakerStatus::Nominator(vec![3, 4])),
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ExtBuilder {
|
||||
pub fn add_staker(
|
||||
mut self,
|
||||
stash: AccountId,
|
||||
ctrl: AccountId,
|
||||
stake: Balance,
|
||||
status: StakerStatus<AccountId>,
|
||||
) -> Self {
|
||||
self.stakers.push((stash, ctrl, stake, status));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> pezsp_io::TestExternalities {
|
||||
let mut storage = pezframe_system::GenesisConfig::<Test>::default().build_storage().unwrap();
|
||||
|
||||
let mut balances: Vec<(AccountId, Balance)> = vec![
|
||||
(1, 1_000_000 * UNITS),
|
||||
(2, 1_000_000 * UNITS),
|
||||
// USER_STASH (10) için de başlangıçta yeterli bakiye atıyoruz,
|
||||
// çünkü testlerde bond etmesi beklenecek.
|
||||
(10, 1_000_000 * UNITS),
|
||||
(20, 100_000 * UNITS),
|
||||
(101, 2_000 * UNITS),
|
||||
];
|
||||
// ExtBuilder'daki tüm staker'ların ve diğer test hesaplarının (eğer varsa)
|
||||
// yeterli bakiyeye sahip olduğundan emin olun.
|
||||
// Her staker'a veya test hesabına minimum bond miktarının çok üzerinde bakiye ekle.
|
||||
for (stash, _, _, _) in &self.stakers {
|
||||
if !balances.iter().any(|(acc, _)| acc == stash) {
|
||||
balances.push((*stash, 1_000_000 * UNITS)); // Staker'lara bol miktarda bakiye
|
||||
}
|
||||
}
|
||||
|
||||
pezpallet_balances::GenesisConfig::<Test> { balances, ..Default::default() }
|
||||
.assimilate_storage(&mut storage)
|
||||
.unwrap();
|
||||
|
||||
pezpallet_staking::GenesisConfig::<Test> {
|
||||
stakers: self.stakers.clone(),
|
||||
validator_count: self.stakers.len() as u32, // Staker sayısını dinamik yap
|
||||
minimum_validator_count: 0, // En az 0 validator olmasına izin ver
|
||||
invulnerables: self
|
||||
.stakers
|
||||
.iter()
|
||||
.filter_map(|(stash, _, _, status)| {
|
||||
if let StakerStatus::Validator = status {
|
||||
Some(stash.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
force_era: pezpallet_staking::Forcing::ForceNew, // Yeni era başlatmaya zorla
|
||||
min_nominator_bond: MinNominatorBond::get(), // Tanımlanan minimum değerleri kullan
|
||||
min_validator_bond: MinValidatorBond::get(), // Tanımlanan minimum değerleri kullan
|
||||
..Default::default()
|
||||
}
|
||||
.assimilate_storage(&mut storage)
|
||||
.unwrap();
|
||||
|
||||
pezpallet_session::GenesisConfig::<Test> {
|
||||
keys: self
|
||||
.stakers
|
||||
.iter()
|
||||
.filter_map(|(stash, ctrl, _, status)| {
|
||||
if let StakerStatus::Validator = status {
|
||||
Some((*stash, *ctrl, MockSessionKeys { dummy: (*stash).into() }))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
..Default::default()
|
||||
}
|
||||
.assimilate_storage(&mut storage)
|
||||
.unwrap();
|
||||
|
||||
let mut ext = pezsp_io::TestExternalities::new(storage);
|
||||
// run_to_block çağrısını ExtBuilder::build_and_execute içinde veya
|
||||
// benchmarking setup'ında yapmak daha doğru. Burada sadece temel storage'ı kuruyoruz.
|
||||
ext
|
||||
}
|
||||
|
||||
pub fn build_and_execute(self, test: impl FnOnce() -> ()) {
|
||||
self.build().execute_with(test);
|
||||
}
|
||||
}
|
||||
|
||||
/// Bloğu `n`'e kadar ilerletir.
|
||||
pub fn run_to_block(n: BlockNumber) {
|
||||
while System::block_number() < n {
|
||||
if System::block_number() > 1 {
|
||||
System::on_finalize(System::block_number());
|
||||
Session::on_finalize(System::block_number());
|
||||
Staking::on_finalize(System::block_number());
|
||||
}
|
||||
System::set_block_number(System::block_number() + 1);
|
||||
System::on_initialize(System::block_number());
|
||||
Session::on_initialize(System::block_number());
|
||||
Staking::on_initialize(System::block_number());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,359 @@
|
||||
//! pezpallet-staking-score için testler.
|
||||
|
||||
use crate::{mock::*, Error, Event, StakingScoreProvider, MONTH_IN_BLOCKS, UNITS};
|
||||
use pezframe_support::{assert_noop, assert_ok};
|
||||
use pezpallet_staking::RewardDestination;
|
||||
|
||||
// Testlerde kullanacağımız sabitler
|
||||
const USER_STASH: AccountId = 10;
|
||||
const USER_CONTROLLER: AccountId = 10;
|
||||
|
||||
#[test]
|
||||
fn zero_stake_should_return_zero_score() {
|
||||
ExtBuilder::default().build_and_execute(|| {
|
||||
// ExtBuilder'da 10 numaralı hesap için bir staker oluşturmadık.
|
||||
// Bu nedenle, palet 0 puan vermelidir.
|
||||
assert_eq!(StakingScore::get_staking_score(&USER_STASH).0, 0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn score_is_calculated_correctly_without_time_tracking() {
|
||||
ExtBuilder::default().build_and_execute(|| {
|
||||
// 50 HEZ stake edelim. Staking::bond çağrısı ile stake işlemini başlat.
|
||||
assert_ok!(Staking::bond(
|
||||
RuntimeOrigin::signed(USER_STASH),
|
||||
50 * UNITS,
|
||||
RewardDestination::Staked
|
||||
));
|
||||
|
||||
// Süre takibi yokken, puan sadece miktara göre hesaplanmalı (20 puan).
|
||||
assert_eq!(StakingScore::get_staking_score(&USER_STASH).0, 20);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn start_score_tracking_works_and_enables_duration_multiplier() {
|
||||
ExtBuilder::default().build_and_execute(|| {
|
||||
// --- 1. Kurulum ve Başlangıç ---
|
||||
let initial_block = 10;
|
||||
System::set_block_number(initial_block);
|
||||
|
||||
// 500 HEZ stake edelim. Bu, 40 temel puan demektir.
|
||||
assert_ok!(Staking::bond(
|
||||
RuntimeOrigin::signed(USER_STASH),
|
||||
500 * UNITS,
|
||||
RewardDestination::Staked
|
||||
));
|
||||
|
||||
// Eylem: Süre takibini başlat. Depolamaya `10` yazılacak.
|
||||
assert_ok!(StakingScore::start_score_tracking(RuntimeOrigin::signed(USER_STASH)));
|
||||
|
||||
// Doğrulama: Başlangıç puanı doğru mu?
|
||||
assert_eq!(
|
||||
StakingScore::get_staking_score(&USER_STASH).0,
|
||||
40,
|
||||
"Initial score should be 40"
|
||||
);
|
||||
|
||||
// --- 2. Dört Ay Sonrası ---
|
||||
let target_block_4m = initial_block + (4 * MONTH_IN_BLOCKS) as u64;
|
||||
let expected_duration_4m = target_block_4m - initial_block;
|
||||
// Eylem: Zamanı 4 ay ileri "yaşat".
|
||||
System::set_block_number(target_block_4m);
|
||||
|
||||
let (score_4m, duration_4m) = StakingScore::get_staking_score(&USER_STASH);
|
||||
assert_eq!(duration_4m, expected_duration_4m, "Duration after 4 months is wrong");
|
||||
assert_eq!(score_4m, 56, "Score after 4 months should be 56");
|
||||
|
||||
// --- 3. On Üç Ay Sonrası ---
|
||||
let target_block_13m = initial_block + (13 * MONTH_IN_BLOCKS) as u64;
|
||||
let expected_duration_13m = target_block_13m - initial_block;
|
||||
// Eylem: Zamanı başlangıçtan 13 ay sonrasına "yaşat".
|
||||
System::set_block_number(target_block_13m);
|
||||
|
||||
let (score_13m, duration_13m) = StakingScore::get_staking_score(&USER_STASH);
|
||||
assert_eq!(duration_13m, expected_duration_13m, "Duration after 13 months is wrong");
|
||||
assert_eq!(score_13m, 80, "Score after 13 months should be 80");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_staking_score_works_without_explicit_tracking() {
|
||||
ExtBuilder::default().build_and_execute(|| {
|
||||
// 751 HEZ stake edelim. Bu, 50 temel puan demektir.
|
||||
assert_ok!(Staking::bond(
|
||||
RuntimeOrigin::signed(USER_STASH),
|
||||
751 * UNITS,
|
||||
RewardDestination::Staked
|
||||
));
|
||||
|
||||
// Puanın 50 olmasını bekliyoruz.
|
||||
assert_eq!(StakingScore::get_staking_score(&USER_STASH).0, 50);
|
||||
|
||||
// Zamanı ne kadar ileri alırsak alalım, `start_score_tracking` çağrılmadığı
|
||||
// için puan değişmemeli.
|
||||
System::set_block_number(1_000_000_000);
|
||||
assert_eq!(StakingScore::get_staking_score(&USER_STASH).0, 50);
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Amount-Based Scoring Edge Cases (4 tests)
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn amount_score_boundary_100_hez() {
|
||||
ExtBuilder::default().build_and_execute(|| {
|
||||
// Exactly 100 HEZ should give 20 points
|
||||
assert_ok!(Staking::bond(
|
||||
RuntimeOrigin::signed(USER_STASH),
|
||||
100 * UNITS,
|
||||
RewardDestination::Staked
|
||||
));
|
||||
|
||||
assert_eq!(StakingScore::get_staking_score(&USER_STASH).0, 20);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn amount_score_boundary_250_hez() {
|
||||
ExtBuilder::default().build_and_execute(|| {
|
||||
// Exactly 250 HEZ should give 30 points
|
||||
assert_ok!(Staking::bond(
|
||||
RuntimeOrigin::signed(USER_STASH),
|
||||
250 * UNITS,
|
||||
RewardDestination::Staked
|
||||
));
|
||||
|
||||
assert_eq!(StakingScore::get_staking_score(&USER_STASH).0, 30);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn amount_score_boundary_750_hez() {
|
||||
ExtBuilder::default().build_and_execute(|| {
|
||||
// Exactly 750 HEZ should give 40 points
|
||||
assert_ok!(Staking::bond(
|
||||
RuntimeOrigin::signed(USER_STASH),
|
||||
750 * UNITS,
|
||||
RewardDestination::Staked
|
||||
));
|
||||
|
||||
assert_eq!(StakingScore::get_staking_score(&USER_STASH).0, 40);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn score_capped_at_100() {
|
||||
ExtBuilder::default().build_and_execute(|| {
|
||||
// Stake maximum amount and advance time to get maximum multiplier
|
||||
assert_ok!(Staking::bond(
|
||||
RuntimeOrigin::signed(USER_STASH),
|
||||
1000 * UNITS, // 50 base points
|
||||
RewardDestination::Staked
|
||||
));
|
||||
|
||||
assert_ok!(StakingScore::start_score_tracking(RuntimeOrigin::signed(USER_STASH)));
|
||||
|
||||
// Advance 12+ months to get 2.0x multiplier
|
||||
System::set_block_number((12 * MONTH_IN_BLOCKS + 1) as u64);
|
||||
|
||||
// 50 * 2.0 = 100, should be capped at 100
|
||||
let (score, _) = StakingScore::get_staking_score(&USER_STASH);
|
||||
assert_eq!(score, 100);
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Duration Multiplier Tests (3 tests)
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn duration_multiplier_1_month() {
|
||||
ExtBuilder::default().build_and_execute(|| {
|
||||
assert_ok!(Staking::bond(
|
||||
RuntimeOrigin::signed(USER_STASH),
|
||||
500 * UNITS, // 40 base points
|
||||
RewardDestination::Staked
|
||||
));
|
||||
|
||||
assert_ok!(StakingScore::start_score_tracking(RuntimeOrigin::signed(USER_STASH)));
|
||||
|
||||
// Advance 1 month
|
||||
System::set_block_number((1 * MONTH_IN_BLOCKS + 1) as u64);
|
||||
|
||||
// 40 * 1.2 = 48
|
||||
let (score, _) = StakingScore::get_staking_score(&USER_STASH);
|
||||
assert_eq!(score, 48);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_multiplier_6_months() {
|
||||
ExtBuilder::default().build_and_execute(|| {
|
||||
assert_ok!(Staking::bond(
|
||||
RuntimeOrigin::signed(USER_STASH),
|
||||
500 * UNITS, // 40 base points
|
||||
RewardDestination::Staked
|
||||
));
|
||||
|
||||
assert_ok!(StakingScore::start_score_tracking(RuntimeOrigin::signed(USER_STASH)));
|
||||
|
||||
// Advance 6 months
|
||||
System::set_block_number((6 * MONTH_IN_BLOCKS + 1) as u64);
|
||||
|
||||
// 40 * 1.7 = 68
|
||||
let (score, _) = StakingScore::get_staking_score(&USER_STASH);
|
||||
assert_eq!(score, 68);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_multiplier_progression() {
|
||||
ExtBuilder::default().build_and_execute(|| {
|
||||
let base_block = 100;
|
||||
System::set_block_number(base_block);
|
||||
|
||||
assert_ok!(Staking::bond(
|
||||
RuntimeOrigin::signed(USER_STASH),
|
||||
100 * UNITS, // 20 base points
|
||||
RewardDestination::Staked
|
||||
));
|
||||
|
||||
assert_ok!(StakingScore::start_score_tracking(RuntimeOrigin::signed(USER_STASH)));
|
||||
|
||||
// Start: 20 * 1.0 = 20
|
||||
assert_eq!(StakingScore::get_staking_score(&USER_STASH).0, 20);
|
||||
|
||||
// After 3 months: 20 * 1.4 = 28
|
||||
System::set_block_number(base_block + (3 * MONTH_IN_BLOCKS) as u64);
|
||||
assert_eq!(StakingScore::get_staking_score(&USER_STASH).0, 28);
|
||||
|
||||
// After 12 months: 20 * 2.0 = 40
|
||||
System::set_block_number(base_block + (12 * MONTH_IN_BLOCKS) as u64);
|
||||
assert_eq!(StakingScore::get_staking_score(&USER_STASH).0, 40);
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// start_score_tracking Extrinsic Tests (3 tests)
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn start_tracking_fails_without_stake() {
|
||||
ExtBuilder::default().build_and_execute(|| {
|
||||
// Try to start tracking without any stake
|
||||
assert_noop!(
|
||||
StakingScore::start_score_tracking(RuntimeOrigin::signed(USER_STASH)),
|
||||
Error::<Test>::NoStakeFound
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn start_tracking_fails_if_already_started() {
|
||||
ExtBuilder::default().build_and_execute(|| {
|
||||
assert_ok!(Staking::bond(
|
||||
RuntimeOrigin::signed(USER_STASH),
|
||||
100 * UNITS,
|
||||
RewardDestination::Staked
|
||||
));
|
||||
|
||||
// First call succeeds
|
||||
assert_ok!(StakingScore::start_score_tracking(RuntimeOrigin::signed(USER_STASH)));
|
||||
|
||||
// Second call fails
|
||||
assert_noop!(
|
||||
StakingScore::start_score_tracking(RuntimeOrigin::signed(USER_STASH)),
|
||||
Error::<Test>::TrackingAlreadyStarted
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn start_tracking_emits_event() {
|
||||
ExtBuilder::default().build_and_execute(|| {
|
||||
System::set_block_number(1);
|
||||
|
||||
assert_ok!(Staking::bond(
|
||||
RuntimeOrigin::signed(USER_STASH),
|
||||
100 * UNITS,
|
||||
RewardDestination::Staked
|
||||
));
|
||||
|
||||
assert_ok!(StakingScore::start_score_tracking(RuntimeOrigin::signed(USER_STASH)));
|
||||
|
||||
// Check event was emitted
|
||||
let events = System::events();
|
||||
assert!(events.iter().any(|event| {
|
||||
matches!(event.event, RuntimeEvent::StakingScore(Event::ScoreTrackingStarted { .. }))
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Edge Cases and Integration (2 tests)
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn multiple_users_independent_scores() {
|
||||
ExtBuilder::default().build_and_execute(|| {
|
||||
// Use USER_STASH (10) and account 11 which have pre-allocated balances
|
||||
let user1 = USER_STASH; // Account 10
|
||||
let user2 = 11; // Account 11 (already has stake in mock)
|
||||
|
||||
// User1: Add new stake, no tracking
|
||||
assert_ok!(Staking::bond(
|
||||
RuntimeOrigin::signed(user1),
|
||||
100 * UNITS,
|
||||
RewardDestination::Staked
|
||||
));
|
||||
|
||||
// User2 already has stake from mock (100 HEZ)
|
||||
// Start tracking for user2
|
||||
assert_ok!(StakingScore::start_score_tracking(RuntimeOrigin::signed(user2)));
|
||||
|
||||
// User1 should have base score of 20 (100 HEZ)
|
||||
assert_eq!(StakingScore::get_staking_score(&user1).0, 20);
|
||||
|
||||
// User2 should have base score of 20 (100 HEZ from mock)
|
||||
assert_eq!(StakingScore::get_staking_score(&user2).0, 20);
|
||||
|
||||
// Advance time
|
||||
System::set_block_number((3 * MONTH_IN_BLOCKS) as u64);
|
||||
|
||||
// User1 score unchanged (no tracking)
|
||||
assert_eq!(StakingScore::get_staking_score(&user1).0, 20);
|
||||
|
||||
// User2 score increased (20 * 1.4 = 28)
|
||||
assert_eq!(StakingScore::get_staking_score(&user2).0, 28);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_returned_correctly() {
|
||||
ExtBuilder::default().build_and_execute(|| {
|
||||
let start_block = 100;
|
||||
System::set_block_number(start_block);
|
||||
|
||||
assert_ok!(Staking::bond(
|
||||
RuntimeOrigin::signed(USER_STASH),
|
||||
100 * UNITS,
|
||||
RewardDestination::Staked
|
||||
));
|
||||
|
||||
// Without tracking, duration should be 0
|
||||
let (_, duration) = StakingScore::get_staking_score(&USER_STASH);
|
||||
assert_eq!(duration, 0);
|
||||
|
||||
assert_ok!(StakingScore::start_score_tracking(RuntimeOrigin::signed(USER_STASH)));
|
||||
|
||||
// After 5 months
|
||||
let target_block = start_block + (5 * MONTH_IN_BLOCKS) as u64;
|
||||
System::set_block_number(target_block);
|
||||
|
||||
let (_, duration) = StakingScore::get_staking_score(&USER_STASH);
|
||||
assert_eq!(duration, target_block - start_block);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
|
||||
//! Autogenerated weights for `pezpallet_staking_score`
|
||||
//!
|
||||
//! THIS FILE WAS AUTO-GENERATED USING THE BIZINIKIWI BENCHMARK CLI VERSION 32.0.0
|
||||
//! DATE: 2025-12-08, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
|
||||
//! WORST CASE MAP SIZE: `1000000`
|
||||
//! HOSTNAME: `MamostePC`, CPU: `11th Gen Intel(R) Core(TM) i9-11950H @ 2.60GHz`
|
||||
//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: `1024`
|
||||
|
||||
// Executed Command:
|
||||
// ./target/release/frame-omni-bencher
|
||||
// v1
|
||||
// benchmark
|
||||
// pallet
|
||||
// --runtime
|
||||
// target/release/wbuild/people-pezkuwichain-runtime/people_pezkuwichain_runtime.compact.compressed.wasm
|
||||
// --pallets
|
||||
// pezpallet_staking_score
|
||||
// -e
|
||||
// all
|
||||
// --steps
|
||||
// 50
|
||||
// --repeat
|
||||
// 20
|
||||
// --output
|
||||
// pezcumulus/teyrchains/pallets/staking-score/src/weights.rs
|
||||
// --template
|
||||
// bizinikiwi/.maintain/frame-weight-template.hbs
|
||||
|
||||
#![cfg_attr(rustfmt, rustfmt_skip)]
|
||||
#![allow(unused_parens)]
|
||||
#![allow(unused_imports)]
|
||||
#![allow(missing_docs)]
|
||||
#![allow(dead_code)]
|
||||
|
||||
use pezframe_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}};
|
||||
use core::marker::PhantomData;
|
||||
|
||||
/// Weight functions needed for `pezpallet_staking_score`.
|
||||
pub trait WeightInfo {
|
||||
fn start_score_tracking() -> Weight;
|
||||
}
|
||||
|
||||
/// Weights for `pezpallet_staking_score` using the Bizinikiwi node and recommended hardware.
|
||||
pub struct BizinikiwiWeight<T>(PhantomData<T>);
|
||||
impl<T: pezframe_system::Config> WeightInfo for BizinikiwiWeight<T> {
|
||||
/// Storage: `StakingScore::StakingStartBlock` (r:1 w:1)
|
||||
/// Proof: `StakingScore::StakingStartBlock` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`)
|
||||
fn start_score_tracking() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `76`
|
||||
// Estimated: `3517`
|
||||
// Minimum execution time: 11_419_000 picoseconds.
|
||||
Weight::from_parts(11_860_000, 3517)
|
||||
.saturating_add(T::DbWeight::get().reads(1_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(1_u64))
|
||||
}
|
||||
}
|
||||
|
||||
// For backwards compatibility and tests.
|
||||
impl WeightInfo for () {
|
||||
/// Storage: `StakingScore::StakingStartBlock` (r:1 w:1)
|
||||
/// Proof: `StakingScore::StakingStartBlock` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`)
|
||||
fn start_score_tracking() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `76`
|
||||
// Estimated: `3517`
|
||||
// Minimum execution time: 11_419_000 picoseconds.
|
||||
Weight::from_parts(11_860_000, 3517)
|
||||
.saturating_add(RocksDbWeight::get().reads(1_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(1_u64))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
[package]
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
name = "pezstaging-teyrchain-info"
|
||||
version = "0.7.0"
|
||||
license = "Apache-2.0"
|
||||
description = "Pallet to store the teyrchain ID"
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
codec = { features = ["derive"], workspace = true }
|
||||
scale-info = { features = ["derive"], workspace = true }
|
||||
|
||||
pezframe-support = { workspace = true }
|
||||
pezframe-system = { workspace = true }
|
||||
|
||||
pezsp-runtime = { workspace = true }
|
||||
|
||||
pezcumulus-primitives-core = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = [
|
||||
"codec/std",
|
||||
"pezcumulus-primitives-core/std",
|
||||
"pezframe-support/std",
|
||||
"pezframe-system/std",
|
||||
"scale-info/std",
|
||||
"pezsp-runtime/std",
|
||||
]
|
||||
try-runtime = [
|
||||
"pezframe-support/try-runtime",
|
||||
"pezframe-system/try-runtime",
|
||||
"pezsp-runtime/try-runtime",
|
||||
]
|
||||
runtime-benchmarks = [
|
||||
"pezcumulus-primitives-core/runtime-benchmarks",
|
||||
"pezframe-support/runtime-benchmarks",
|
||||
"pezframe-system/runtime-benchmarks",
|
||||
"pezsp-runtime/runtime-benchmarks",
|
||||
]
|
||||
@@ -0,0 +1,81 @@
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// This file is part of Pezcumulus.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Minimal Pallet that injects a TeyrchainId into Runtime storage from
|
||||
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
pub use pallet::*;
|
||||
|
||||
#[pezframe_support::pallet]
|
||||
pub mod pallet {
|
||||
use cumulus_primitives_core::ParaId;
|
||||
use pezframe_support::pezpallet_prelude::*;
|
||||
use pezframe_system::pezpallet_prelude::*;
|
||||
|
||||
#[pallet::pallet]
|
||||
pub struct Pallet<T>(_);
|
||||
|
||||
#[pallet::config]
|
||||
pub trait Config: pezframe_system::Config {}
|
||||
|
||||
#[pallet::hooks]
|
||||
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {}
|
||||
|
||||
#[pallet::call]
|
||||
impl<T: Config> Pallet<T> {}
|
||||
|
||||
#[pallet::genesis_config]
|
||||
pub struct GenesisConfig<T: Config> {
|
||||
#[serde(skip)]
|
||||
pub _config: core::marker::PhantomData<T>,
|
||||
pub teyrchain_id: ParaId,
|
||||
}
|
||||
|
||||
impl<T: Config> Default for GenesisConfig<T> {
|
||||
fn default() -> Self {
|
||||
Self { teyrchain_id: 100.into(), _config: Default::default() }
|
||||
}
|
||||
}
|
||||
|
||||
#[pallet::genesis_build]
|
||||
impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
|
||||
fn build(&self) {
|
||||
TeyrchainId::<T>::put(self.teyrchain_id);
|
||||
}
|
||||
}
|
||||
|
||||
#[pallet::type_value]
|
||||
pub(super) fn DefaultForTeyrchainId() -> ParaId {
|
||||
100.into()
|
||||
}
|
||||
|
||||
#[pallet::storage]
|
||||
pub(super) type TeyrchainId<T: Config> =
|
||||
StorageValue<_, ParaId, ValueQuery, DefaultForTeyrchainId>;
|
||||
|
||||
impl<T: Config> Get<ParaId> for Pallet<T> {
|
||||
fn get() -> ParaId {
|
||||
TeyrchainId::<T>::get()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> Pallet<T> {
|
||||
pub fn teyrchain_id() -> ParaId {
|
||||
TeyrchainId::<T>::get()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
[package]
|
||||
name = "pezpallet-tiki"
|
||||
description = "PezkuwiChain Tiki (Role NFT) Management Pallet"
|
||||
version = "1.0.0"
|
||||
authors.workspace = true
|
||||
homepage.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
publish = false
|
||||
repository.workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
||||
[dependencies]
|
||||
codec = { workspace = true, default-features = false, features = ["derive"] }
|
||||
pezframe-benchmarking = { optional = true, workspace = true }
|
||||
pezframe-support = { default-features = false, workspace = true }
|
||||
pezframe-system = { default-features = false, workspace = true }
|
||||
log = { default-features = false, workspace = true }
|
||||
pezpallet-balances = { default-features = false, optional = true, workspace = true }
|
||||
pezpallet-identity = { default-features = false, workspace = true }
|
||||
pezpallet-identity-kyc = { workspace = true, default-features = false }
|
||||
pezpallet-nfts = { default-features = false, workspace = true }
|
||||
scale-info = { default-features = false, features = [
|
||||
"derive",
|
||||
], workspace = true }
|
||||
serde = { version = "1.0", default-features = false, features = ["derive"] }
|
||||
pezsp-runtime = { default-features = false, workspace = true }
|
||||
pezsp-std = { default-features = false, workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
pezsp-core = { workspace = true }
|
||||
pezsp-io = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = [
|
||||
"codec/std",
|
||||
"pezframe-benchmarking?/std",
|
||||
"pezframe-support/std",
|
||||
"pezframe-system/std",
|
||||
"log/std",
|
||||
"pezpallet-balances/std",
|
||||
"pezpallet-identity-kyc/std",
|
||||
"pezpallet-identity/std",
|
||||
"pezpallet-nfts/std",
|
||||
"scale-info/std",
|
||||
"serde/std",
|
||||
"pezsp-core/std",
|
||||
"pezsp-io/std",
|
||||
"pezsp-runtime/std",
|
||||
"pezsp-std/std",
|
||||
]
|
||||
runtime-benchmarks = [
|
||||
"dep:pezframe-benchmarking",
|
||||
"dep:pezpallet-balances",
|
||||
"pezframe-benchmarking/runtime-benchmarks",
|
||||
"pezframe-support/runtime-benchmarks",
|
||||
"pezframe-system/runtime-benchmarks",
|
||||
"pezpallet-balances/runtime-benchmarks",
|
||||
"pezpallet-identity-kyc/runtime-benchmarks",
|
||||
"pezpallet-identity/runtime-benchmarks",
|
||||
"pezpallet-nfts/runtime-benchmarks",
|
||||
"pezsp-io/runtime-benchmarks",
|
||||
"pezsp-runtime/runtime-benchmarks",
|
||||
]
|
||||
try-runtime = [
|
||||
"pezframe-support/try-runtime",
|
||||
"pezframe-system/try-runtime",
|
||||
"pezpallet-balances/try-runtime",
|
||||
"pezpallet-identity-kyc/try-runtime",
|
||||
"pezpallet-identity/try-runtime",
|
||||
"pezpallet-nfts/try-runtime",
|
||||
"pezsp-runtime/try-runtime",
|
||||
]
|
||||
@@ -0,0 +1,174 @@
|
||||
//! Benchmarking setup for pezpallet-tiki
|
||||
use super::*;
|
||||
|
||||
#[allow(unused)]
|
||||
use crate::Pallet as Tiki;
|
||||
use pezframe_benchmarking::v2::*;
|
||||
use pezframe_system::RawOrigin;
|
||||
// Gerekli trait'leri import ediyoruz
|
||||
use pezframe_support::traits::{Currency, Get};
|
||||
use pezpallet_balances::Pallet as Balances;
|
||||
use pezsp_runtime::traits::StaticLookup;
|
||||
extern crate alloc;
|
||||
use alloc::vec;
|
||||
|
||||
// Gerekli trait kısıtlamalarını ana benchmarks bloğuna ekliyoruz.
|
||||
#[benchmarks(
|
||||
where
|
||||
T::CollectionId: Copy + Default + PartialOrd,
|
||||
T: pezpallet_balances::Config,
|
||||
)]
|
||||
mod benchmarks {
|
||||
use super::*;
|
||||
|
||||
// Bu yardımcı fonksiyon, runtime'da tanımlanan Tiki koleksiyonunu oluşturur.
|
||||
fn ensure_collection_exists<T: Config>()
|
||||
where
|
||||
T::CollectionId: Copy + Default + PartialOrd,
|
||||
T: pezpallet_balances::Config,
|
||||
{
|
||||
let collection_id = T::TikiCollectionId::get();
|
||||
// Koleksiyon sahibi olarak fonlanmış `whitelisted_caller`'ı kullanıyoruz.
|
||||
let caller: T::AccountId = whitelisted_caller();
|
||||
|
||||
// Fund the caller account with sufficient balance for NFT deposits
|
||||
// Use a very large balance to ensure all deposit requirements can be met
|
||||
let funding = Balances::<T>::minimum_balance() * 1_000_000_000u32.into();
|
||||
Balances::<T>::make_free_balance_be(&caller, funding);
|
||||
|
||||
// `while` döngüsü, 'Step' trait'ine olan ihtiyacı ortadan kaldırır.
|
||||
while pezpallet_nfts::NextCollectionId::<T>::get().unwrap_or_default() <= collection_id {
|
||||
let _ = pezpallet_nfts::Pallet::<T>::force_create(
|
||||
RawOrigin::Root.into(),
|
||||
T::Lookup::unlookup(caller.clone()),
|
||||
pezpallet_nfts::CollectionConfig {
|
||||
settings: Default::default(),
|
||||
max_supply: None,
|
||||
mint_settings: Default::default(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to ensure user has a citizen NFT
|
||||
fn ensure_citizen_nft<T: Config>(who: T::AccountId) -> Result<(), DispatchError>
|
||||
where
|
||||
T::CollectionId: Copy + Default + PartialOrd,
|
||||
T: pezpallet_balances::Config,
|
||||
{
|
||||
ensure_collection_exists::<T>();
|
||||
|
||||
// Fund the user account with sufficient balance for NFT deposits
|
||||
// Use a very large balance to ensure all deposit requirements can be met
|
||||
let funding = Balances::<T>::minimum_balance() * 1_000_000_000u32.into();
|
||||
Balances::<T>::make_free_balance_be(&who, funding);
|
||||
|
||||
if Tiki::<T>::citizen_nft(&who).is_none() {
|
||||
Tiki::<T>::mint_citizen_nft_for_user(&who)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn grant_tiki() -> Result<(), BenchmarkError> {
|
||||
// NFT'yi alacak 'dest' hesabı olarak fonlanmış `whitelisted_caller`'ı kullanıyoruz.
|
||||
let dest: T::AccountId = whitelisted_caller();
|
||||
// Appointed role kullan (Serok yerine Wezir)
|
||||
let tiki = crate::Tiki::Wezir;
|
||||
|
||||
// Ensure the dest account has a citizen NFT before granting a tiki
|
||||
ensure_citizen_nft::<T>(dest.clone())?;
|
||||
|
||||
#[extrinsic_call]
|
||||
_(RawOrigin::Root, T::Lookup::unlookup(dest.clone()), tiki.clone());
|
||||
|
||||
// For non-unique roles, check user has the role
|
||||
assert!(Tiki::<T>::user_tikis(&dest).contains(&tiki));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn revoke_tiki() -> Result<(), BenchmarkError> {
|
||||
// NFT'yi alacak 'dest' hesabı olarak fonlanmış `whitelisted_caller`'ı kullanıyoruz.
|
||||
let dest: T::AccountId = whitelisted_caller();
|
||||
let tiki = crate::Tiki::Wezir; // Use appointed role
|
||||
|
||||
// Ensure the dest account has a citizen NFT and the tiki before revoking
|
||||
ensure_citizen_nft::<T>(dest.clone())?;
|
||||
Tiki::<T>::internal_grant_role(&dest, tiki.clone())?; // Use internal function to grant without origin check
|
||||
|
||||
// Verify the role was granted
|
||||
assert!(Tiki::<T>::user_tikis(&dest).contains(&tiki));
|
||||
|
||||
#[extrinsic_call]
|
||||
_(RawOrigin::Root, T::Lookup::unlookup(dest.clone()), tiki.clone());
|
||||
|
||||
// User should no longer have this role
|
||||
assert!(!Tiki::<T>::user_tikis(&dest).contains(&tiki));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn force_mint_citizen_nft() -> Result<(), BenchmarkError> {
|
||||
let dest: T::AccountId = whitelisted_caller();
|
||||
|
||||
// Ensure collection exists first
|
||||
ensure_collection_exists::<T>();
|
||||
|
||||
// Henüz vatandaş olmamalı
|
||||
assert!(Tiki::<T>::citizen_nft(&dest).is_none());
|
||||
|
||||
#[extrinsic_call]
|
||||
_(RawOrigin::Root, T::Lookup::unlookup(dest.clone()));
|
||||
|
||||
// Vatandaş olduğundan emin ol
|
||||
assert!(Tiki::<T>::citizen_nft(&dest).is_some());
|
||||
assert!(Tiki::<T>::is_citizen(&dest));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn grant_earned_role() -> Result<(), BenchmarkError> {
|
||||
let dest: T::AccountId = whitelisted_caller();
|
||||
let tiki = crate::Tiki::Axa; // Earned bir rol
|
||||
|
||||
// Ön koşul: Vatandaş olmalı
|
||||
ensure_citizen_nft::<T>(dest.clone())?;
|
||||
|
||||
#[extrinsic_call]
|
||||
_(RawOrigin::Root, T::Lookup::unlookup(dest.clone()), tiki.clone());
|
||||
|
||||
// Rolün verildiğini doğrula
|
||||
assert!(Tiki::<T>::has_tiki(&dest, &tiki));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn grant_elected_role() -> Result<(), BenchmarkError> {
|
||||
let dest: T::AccountId = whitelisted_caller();
|
||||
let tiki = crate::Tiki::Parlementer; // Elected bir rol
|
||||
|
||||
// Ön koşul: Vatandaş olmalı
|
||||
ensure_citizen_nft::<T>(dest.clone())?;
|
||||
|
||||
#[extrinsic_call]
|
||||
_(RawOrigin::Root, T::Lookup::unlookup(dest.clone()), tiki.clone());
|
||||
|
||||
// Rolün verildiğini doğrula
|
||||
assert!(Tiki::<T>::has_tiki(&dest, &tiki));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Temporarily skip this benchmark due to KYC complexity in benchmark environment
|
||||
// #[benchmark]
|
||||
// fn apply_for_citizenship() -> Result<(), BenchmarkError> {
|
||||
// // KYC setup is complex in benchmark environment
|
||||
// // This functionality is covered by force_mint_citizen_nft benchmark
|
||||
// Ok(())
|
||||
// }
|
||||
|
||||
impl_benchmark_test_suite!(Tiki, crate::mock::new_test_ext(), crate::mock::Test);
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
//! Custom Origin verification mechanisms based on Tiki ownership.
|
||||
//!
|
||||
//! This module provides `EnsureOrigin` implementations that verify
|
||||
//! the caller holds a specific Tiki role (Serok, Wezir, or Parlementer).
|
||||
//!
|
||||
//! # Feature Unification Note
|
||||
//!
|
||||
//! Due to Cargo's feature unification behavior, this pallet must be excluded
|
||||
//! from `cargo check --benches` operations when its `runtime-benchmarks` feature
|
||||
//! is not explicitly enabled. The CI workflow (tests-misc.yml) handles this
|
||||
//! by excluding pezpallet-tiki and all its dependents.
|
||||
//!
|
||||
//! The issue: When building with `--benches`, other packages may enable
|
||||
//! `pezframe-support/runtime-benchmarks`, which makes `EnsureOrigin::try_successful_origin`
|
||||
//! a required trait method. However, if `pezpallet-tiki/runtime-benchmarks` is not enabled,
|
||||
//! our cfg-gated method won't be compiled, causing E0046 errors.
|
||||
//!
|
||||
//! CI exclusion: .github/workflows/tests-misc.yml excludes this pallet with:
|
||||
//! `--exclude pezpallet-tiki` in the `cargo check --benches` command.
|
||||
|
||||
use crate::{Config, Pallet as TikiPallet};
|
||||
use pezframe_support::traits::EnsureOrigin;
|
||||
use pezframe_system::ensure_signed;
|
||||
use pezsp_std::marker::PhantomData;
|
||||
|
||||
// --- Marker Trait for Tiki Roles ---
|
||||
|
||||
/// A trait to return a specific `Tiki` enum variant.
|
||||
///
|
||||
/// This trait is implemented by marker structs to identify which
|
||||
/// Tiki role is required for origin verification.
|
||||
pub trait GetTiki {
|
||||
/// Returns the specific Tiki variant this marker represents.
|
||||
fn tiki() -> crate::Tiki;
|
||||
}
|
||||
|
||||
// --- Marker Structs for Each Role ---
|
||||
|
||||
/// Marker struct representing the `Serok` (President/Leader) role.
|
||||
///
|
||||
/// Use with `EnsureTiki` to require the caller holds the Serok Tiki:
|
||||
/// ```ignore
|
||||
/// type SerokOrigin = EnsureTiki<Runtime, SerokRole>;
|
||||
/// ```
|
||||
pub struct SerokRole;
|
||||
|
||||
impl GetTiki for SerokRole {
|
||||
fn tiki() -> crate::Tiki {
|
||||
crate::Tiki::Serok
|
||||
}
|
||||
}
|
||||
|
||||
/// Marker struct representing the `Wezir` (Minister/Advisor) role.
|
||||
///
|
||||
/// Use with `EnsureTiki` to require the caller holds the Wezir Tiki:
|
||||
/// ```ignore
|
||||
/// type WezirOrigin = EnsureTiki<Runtime, WezirRole>;
|
||||
/// ```
|
||||
pub struct WezirRole;
|
||||
|
||||
impl GetTiki for WezirRole {
|
||||
fn tiki() -> crate::Tiki {
|
||||
crate::Tiki::Wezir
|
||||
}
|
||||
}
|
||||
|
||||
/// Marker struct representing the `Parlementer` (Parliamentarian) role.
|
||||
///
|
||||
/// Use with `EnsureTiki` to require the caller holds the Parlementer Tiki:
|
||||
/// ```ignore
|
||||
/// type ParlementerOrigin = EnsureTiki<Runtime, ParlementerRole>;
|
||||
/// ```
|
||||
pub struct ParlementerRole;
|
||||
|
||||
impl GetTiki for ParlementerRole {
|
||||
fn tiki() -> crate::Tiki {
|
||||
crate::Tiki::Parlementer
|
||||
}
|
||||
}
|
||||
|
||||
// --- EnsureOrigin Implementation ---
|
||||
|
||||
/// An `EnsureOrigin` implementation that requires ownership of a specific Tiki.
|
||||
///
|
||||
/// This struct verifies that the origin is a signed account that currently
|
||||
/// holds the Tiki role specified by the `I: GetTiki` type parameter.
|
||||
///
|
||||
/// # Type Parameters
|
||||
///
|
||||
/// * `T` - The runtime configuration type implementing `Config`
|
||||
/// * `I` - A marker type implementing `GetTiki` to specify which Tiki role is required
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```ignore
|
||||
/// // Require the caller to hold the Serok Tiki
|
||||
/// type SerokOrigin = EnsureTiki<Runtime, SerokRole>;
|
||||
///
|
||||
/// // Use in a pallet's dispatchable
|
||||
/// #[pallet::call]
|
||||
/// impl<T: Config> Pallet<T> {
|
||||
/// pub fn privileged_action(origin: OriginFor<T>) -> DispatchResult {
|
||||
/// let who = T::SerokOrigin::ensure_origin(origin)?;
|
||||
/// // ... action requiring Serok authority
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub struct EnsureTiki<T, I>(PhantomData<(T, I)>);
|
||||
|
||||
impl<T, I> EnsureOrigin<T::RuntimeOrigin> for EnsureTiki<T, I>
|
||||
where
|
||||
T: Config,
|
||||
I: GetTiki,
|
||||
{
|
||||
type Success = T::AccountId;
|
||||
|
||||
fn try_origin(o: T::RuntimeOrigin) -> Result<Self::Success, T::RuntimeOrigin> {
|
||||
// First, verify the origin is a signed account
|
||||
let who = match ensure_signed(o.clone()) {
|
||||
Ok(account) => account,
|
||||
Err(_) => return Err(o),
|
||||
};
|
||||
|
||||
// Get the required Tiki role from the marker type
|
||||
let required_tiki = I::tiki();
|
||||
|
||||
// Check if the caller currently holds this Tiki
|
||||
match TikiPallet::<T>::tiki_holder(required_tiki) {
|
||||
Some(holder) if holder == who => Ok(who),
|
||||
_ => Err(o),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
fn try_successful_origin() -> Result<T::RuntimeOrigin, ()> {
|
||||
use codec::Decode;
|
||||
use pezsp_runtime::traits::TrailingZeroInput;
|
||||
|
||||
// Generate a deterministic zero-filled account for benchmarking
|
||||
let zero_account = T::AccountId::decode(&mut TrailingZeroInput::zeroes())
|
||||
.expect("infinite length input; no invalid inputs for type; qed");
|
||||
|
||||
Ok(T::RuntimeOrigin::from(pezframe_system::RawOrigin::Signed(zero_account)))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
impl<T, I> pezframe_support::traits::EnsureOriginWithArg<T::RuntimeOrigin, ()> for EnsureTiki<T, I>
|
||||
where
|
||||
T: Config,
|
||||
I: GetTiki,
|
||||
{
|
||||
type Success = T::AccountId;
|
||||
|
||||
fn try_origin(o: T::RuntimeOrigin, _: &()) -> Result<Self::Success, T::RuntimeOrigin> {
|
||||
<Self as EnsureOrigin<T::RuntimeOrigin>>::try_origin(o)
|
||||
}
|
||||
|
||||
fn try_successful_origin(_: &()) -> Result<T::RuntimeOrigin, ()> {
|
||||
use codec::Decode;
|
||||
use pezsp_runtime::traits::TrailingZeroInput;
|
||||
|
||||
// Generate a deterministic zero-filled account for benchmarking
|
||||
let zero_account = T::AccountId::decode(&mut TrailingZeroInput::zeroes())
|
||||
.expect("infinite length input; no invalid inputs for type; qed");
|
||||
|
||||
Ok(T::RuntimeOrigin::from(pezframe_system::RawOrigin::Signed(zero_account)))
|
||||
}
|
||||
}
|
||||
|
||||
// Convenience type aliases
|
||||
pub type EnsureSerok<T> = EnsureTiki<T, SerokRole>;
|
||||
pub type EnsureWezir<T> = EnsureTiki<T, WezirRole>;
|
||||
pub type EnsureParlementer<T> = EnsureTiki<T, ParlementerRole>;
|
||||
@@ -0,0 +1,864 @@
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
//! # Tiki (Role) Pallet
|
||||
//!
|
||||
//! A pallet for managing citizenship and role-based NFTs with automated and governance-driven
|
||||
//! assignment.
|
||||
//!
|
||||
//! ## Overview
|
||||
//!
|
||||
//! The Tiki pallet implements a comprehensive role management system using non-transferable NFTs
|
||||
//! to represent citizenship status and various roles within the ecosystem. Each role grants
|
||||
//! specific permissions, rights, and social standing.
|
||||
//!
|
||||
//! ## Core Concepts
|
||||
//!
|
||||
//! ### Citizenship NFT
|
||||
//! - Automatically minted upon KYC approval
|
||||
//! - Represents "Welati" (Citizen) status
|
||||
//! - Non-transferable and permanent
|
||||
//! - Required prerequisite for all other roles
|
||||
//!
|
||||
//! ### Role Types (Tiki)
|
||||
//!
|
||||
//! Roles are assigned through different mechanisms:
|
||||
//!
|
||||
//! 1. **Automatic** - System-assigned upon conditions (e.g., Citizenship after KYC)
|
||||
//! 2. **Appointed** - Admin-assigned governmental positions (e.g., Ministers, Judges)
|
||||
//! 3. **Elected** - Community-voted positions (e.g., Parliament members)
|
||||
//! 4. **Earned** - Achievement-based roles (e.g., Educator, Expert)
|
||||
//!
|
||||
//! ### Role Categories
|
||||
//!
|
||||
//! - **Governance**: Serok (President), SerokWeziran (Prime Minister), Ministers
|
||||
//! - **Judicial**: Dadger (Judge), Dozger (Prosecutor), Hiquqnas (Lawyer)
|
||||
//! - **Administrative**: Qeydkar (Registrar), Xezinedar (Treasurer), OperatorêTorê (Network
|
||||
//! Operator)
|
||||
//! - **Educational**: Mamoste (Teacher), Perwerdekar (Educator), Rewsenbîr (Intellectual)
|
||||
//! - **Economic**: Bazargan (Merchant), Navbeynkar (Mediator)
|
||||
//! - **Community**: Parlementer (Parliament Member), ModeratorêCivakê (Community Moderator)
|
||||
//! - **Expert**: Axa (Elder/Expert), Pêseng (Pioneer), Hekem (Wise), Sêwirmend (Counselor)
|
||||
//!
|
||||
//! ## NFT Implementation
|
||||
//!
|
||||
//! - Built on top of `pezpallet-nfts` for standard NFT functionality
|
||||
//! - All Tiki NFTs are non-transferable (soulbound)
|
||||
//! - Transfer attempts are blocked automatically via hooks
|
||||
//! - Each role is represented by a unique NFT item in the TikiCollectionId
|
||||
//!
|
||||
//! ## Role Management
|
||||
//!
|
||||
//! ### Granting Roles
|
||||
//! - Some roles are unique (only one holder at a time)
|
||||
//! - Users can hold multiple compatible roles
|
||||
//! - Maximum roles per user is configurable
|
||||
//! - Trust score requirements for certain roles
|
||||
//!
|
||||
//! ### Revoking Roles
|
||||
//! - Admin can revoke appointed roles
|
||||
//! - Automatic revocation on condition changes
|
||||
//! - Role history maintained for governance transparency
|
||||
//!
|
||||
//! ## Interface
|
||||
//!
|
||||
//! ### Extrinsics
|
||||
//!
|
||||
//! - `grant_tiki(who, tiki, assignment_type)` - Assign a role to a user (admin)
|
||||
//! - `revoke_tiki(who, tiki)` - Remove a role from a user (admin)
|
||||
//! - `force_mint_citizen_nft(who)` - Manually mint citizenship NFT (admin)
|
||||
//!
|
||||
//! ### Storage
|
||||
//!
|
||||
//! - `CitizenNft` - Mapping of accounts to their citizenship NFT IDs
|
||||
//! - `UserTikis` - List of roles held by each user
|
||||
//! - `TikiHolder` - Reverse mapping for unique roles to their holders
|
||||
//! - `NextItemId` - Counter for NFT item ID generation
|
||||
//!
|
||||
//! ### Hooks
|
||||
//!
|
||||
//! - `on_initialize` - Automatic citizenship NFT minting for newly approved KYC users
|
||||
//! - NFT transfer blocking for all Tiki NFTs
|
||||
//!
|
||||
//! ## Dependencies
|
||||
//!
|
||||
//! This pallet requires integration with:
|
||||
//! - `pezpallet-identity-kyc` - KYC status and approval notifications
|
||||
//! - `pezpallet-nfts` - Underlying NFT infrastructure
|
||||
//! - `pezpallet-trust` - Trust score verification for role eligibility
|
||||
//!
|
||||
//! ## Runtime Integration Example
|
||||
//!
|
||||
//! ```ignore
|
||||
//! impl pezpallet_tiki::Config for Runtime {
|
||||
//! type RuntimeEvent = RuntimeEvent;
|
||||
//! type AdminOrigin = EnsureRoot<AccountId>;
|
||||
//! type WeightInfo = pezpallet_tiki::weights::BizinikiwiWeight<Runtime>;
|
||||
//! type TikiCollectionId = ConstU32<1>; // Tiki collection ID
|
||||
//! type MaxTikisPerUser = ConstU32<20>; // Max 20 roles per user
|
||||
//! type Tiki = pezpallet_tiki::Tiki;
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
pub use pallet::*;
|
||||
|
||||
use alloc::{format, vec::Vec};
|
||||
use pezframe_support::pezpallet_prelude::{MaybeSerializeDeserialize, Parameter, RuntimeDebug};
|
||||
use scale_info::TypeInfo;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use pezsp_runtime::DispatchError;
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
mod benchmarking;
|
||||
#[cfg(test)]
|
||||
mod mock;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
pub mod weights;
|
||||
pub use weights::*;
|
||||
pub mod ensure;
|
||||
pub mod migrations; // Storage migrations // For origin validation
|
||||
|
||||
#[pezframe_support::pallet]
|
||||
pub mod pallet {
|
||||
use super::*;
|
||||
use pezframe_support::pezpallet_prelude::*;
|
||||
use pezframe_system::pezpallet_prelude::*;
|
||||
use pezsp_runtime::traits::StaticLookup;
|
||||
|
||||
#[pallet::pallet]
|
||||
#[pallet::storage_version(migrations::STORAGE_VERSION)]
|
||||
pub struct Pallet<T>(_);
|
||||
|
||||
#[pallet::config]
|
||||
pub trait Config:
|
||||
pezframe_system::Config + pezpallet_nfts::Config<ItemId = u32> + pezpallet_identity_kyc::Config
|
||||
{
|
||||
type RuntimeEvent: From<Event<Self>> + IsType<<Self as pezframe_system::Config>::RuntimeEvent>;
|
||||
type AdminOrigin: EnsureOrigin<Self::RuntimeOrigin>;
|
||||
type WeightInfo: weights::WeightInfo;
|
||||
|
||||
/// Collection ID holding Tiki (Role) NFTs.
|
||||
#[pallet::constant]
|
||||
type TikiCollectionId: Get<Self::CollectionId>;
|
||||
|
||||
/// Technical upper limit for maximum number of Tikis (roles) a user can hold.
|
||||
#[pallet::constant]
|
||||
type MaxTikisPerUser: Get<u32>;
|
||||
|
||||
/// Tiki enum type to be used within the pallet.
|
||||
type Tiki: Parameter
|
||||
+ From<Tiki>
|
||||
+ Into<u32>
|
||||
+ MaxEncodedLen
|
||||
+ TypeInfo
|
||||
+ Copy
|
||||
+ MaybeSerializeDeserialize
|
||||
+ 'static;
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Encode,
|
||||
Decode,
|
||||
DecodeWithMemTracking,
|
||||
Clone,
|
||||
Eq,
|
||||
PartialEq,
|
||||
RuntimeDebug,
|
||||
TypeInfo,
|
||||
MaxEncodedLen,
|
||||
Copy,
|
||||
)]
|
||||
pub enum RoleAssignmentType {
|
||||
/// Automatically assigned roles (like Welati after KYC)
|
||||
Automatic,
|
||||
/// Admin-assigned roles (like Wezir, Dadger)
|
||||
Appointed,
|
||||
/// Community-elected roles (like Parlementer) - assigned by pezpallet-voting
|
||||
Elected,
|
||||
/// Earned roles (Axa, roles obtained through exams)
|
||||
Earned,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Encode,
|
||||
Decode,
|
||||
DecodeWithMemTracking,
|
||||
Clone,
|
||||
Eq,
|
||||
PartialEq,
|
||||
RuntimeDebug,
|
||||
TypeInfo,
|
||||
MaxEncodedLen,
|
||||
Copy,
|
||||
)]
|
||||
#[repr(u32)]
|
||||
pub enum Tiki {
|
||||
Welati,
|
||||
Parlementer,
|
||||
SerokiMeclise,
|
||||
Serok,
|
||||
Wezir,
|
||||
EndameDiwane,
|
||||
Dadger,
|
||||
Dozger,
|
||||
Hiquqnas,
|
||||
Noter,
|
||||
Xezinedar,
|
||||
Bacgir,
|
||||
GerinendeyeCavkaniye,
|
||||
OperatorêTorê,
|
||||
PisporêEwlehiyaSîber,
|
||||
GerinendeyeDaneye,
|
||||
Berdevk,
|
||||
Qeydkar,
|
||||
Balyoz,
|
||||
Navbeynkar,
|
||||
ParêzvaneÇandî,
|
||||
Mufetîs,
|
||||
KalîteKontrolker,
|
||||
Mela,
|
||||
Feqî,
|
||||
Perwerdekar,
|
||||
Rewsenbîr,
|
||||
RêveberêProjeyê,
|
||||
SerokêKomele,
|
||||
ModeratorêCivakê,
|
||||
Axa,
|
||||
Pêseng,
|
||||
Sêwirmend,
|
||||
Hekem,
|
||||
Mamoste,
|
||||
// Newly added economic roles
|
||||
Bazargan,
|
||||
// Government roles
|
||||
SerokWeziran,
|
||||
WezireDarayiye,
|
||||
WezireParez,
|
||||
WezireDad,
|
||||
WezireBelaw,
|
||||
WezireTend,
|
||||
WezireAva,
|
||||
WezireCand,
|
||||
}
|
||||
|
||||
impl From<Tiki> for u32 {
|
||||
fn from(val: Tiki) -> Self {
|
||||
val as u32
|
||||
}
|
||||
}
|
||||
|
||||
/// Holds citizenship NFT ID for each user
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn citizen_nft)]
|
||||
pub type CitizenNft<T: Config> =
|
||||
StorageMap<_, Blake2_128Concat, T::AccountId, u32, OptionQuery>;
|
||||
|
||||
/// List of Tikis (roles) owned by each user
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn user_tikis)]
|
||||
pub type UserTikis<T: Config> = StorageMap<
|
||||
_,
|
||||
Blake2_128Concat,
|
||||
T::AccountId,
|
||||
BoundedVec<Tiki, T::MaxTikisPerUser>,
|
||||
ValueQuery,
|
||||
>;
|
||||
|
||||
/// Shows which user a specific Tiki belongs to (for unique roles)
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn tiki_holder)]
|
||||
pub type TikiHolder<T: Config> =
|
||||
StorageMap<_, Blake2_128Concat, Tiki, T::AccountId, OptionQuery>;
|
||||
|
||||
/// Item ID to be used for next NFT
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn next_item_id)]
|
||||
pub type NextItemId<T: Config> = StorageValue<_, u32, ValueQuery>;
|
||||
|
||||
#[pallet::error]
|
||||
pub enum Error<T> {
|
||||
/// Role already belongs to someone else
|
||||
RoleAlreadyTaken,
|
||||
/// Specified person is not the holder of this role
|
||||
NotTheHolder,
|
||||
/// Role not assigned
|
||||
RoleNotAssigned,
|
||||
/// A user has reached maximum role count
|
||||
ExceedsMaxRolesPerUser,
|
||||
/// KYC not completed
|
||||
KycNotCompleted,
|
||||
/// Citizenship NFT already exists
|
||||
CitizenNftAlreadyExists,
|
||||
/// Citizenship NFT not found
|
||||
CitizenNftNotFound,
|
||||
/// User already has this role
|
||||
UserAlreadyHasRole,
|
||||
/// Insufficient Trust Score
|
||||
InsufficientTrustScore,
|
||||
/// This role type cannot be assigned with this method
|
||||
InvalidRoleAssignmentMethod,
|
||||
}
|
||||
|
||||
#[pallet::event]
|
||||
#[pallet::generate_deposit(pub(super) fn deposit_event)]
|
||||
pub enum Event<T: Config> {
|
||||
/// New citizenship NFT minted
|
||||
CitizenNftMinted { who: T::AccountId, nft_id: u32 },
|
||||
/// New Tiki (role) granted
|
||||
TikiGranted { who: T::AccountId, tiki: Tiki },
|
||||
/// Tiki (role) revoked
|
||||
TikiRevoked { who: T::AccountId, tiki: Tiki },
|
||||
/// NFT transfer blocked
|
||||
TransferBlocked {
|
||||
collection_id: T::CollectionId,
|
||||
item_id: u32,
|
||||
from: T::AccountId,
|
||||
to: T::AccountId,
|
||||
},
|
||||
}
|
||||
|
||||
#[pallet::hooks]
|
||||
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
|
||||
fn on_initialize(_block_number: BlockNumberFor<T>) -> Weight {
|
||||
// Check newly KYC-approved users and mint citizenship NFT
|
||||
Self::check_and_mint_citizen_nfts();
|
||||
|
||||
T::DbWeight::get().reads_writes(10, 5)
|
||||
}
|
||||
}
|
||||
|
||||
#[pallet::call]
|
||||
impl<T: Config> Pallet<T> {
|
||||
/// Admin tarafından belirli bir kullanıcıya Tiki (rol) verme
|
||||
#[pallet::call_index(0)]
|
||||
#[pallet::weight(<T as crate::pallet::Config>::WeightInfo::grant_tiki())]
|
||||
pub fn grant_tiki(
|
||||
origin: OriginFor<T>,
|
||||
dest: <T::Lookup as StaticLookup>::Source,
|
||||
tiki: Tiki,
|
||||
) -> DispatchResult {
|
||||
T::AdminOrigin::ensure_origin(origin)?;
|
||||
let dest_account = T::Lookup::lookup(dest)?;
|
||||
|
||||
// Check if the role can be appointed
|
||||
ensure!(
|
||||
Self::can_grant_role_type(&tiki, &RoleAssignmentType::Appointed),
|
||||
Error::<T>::InvalidRoleAssignmentMethod
|
||||
);
|
||||
|
||||
Self::internal_grant_role(&dest_account, tiki)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Admin tarafından belirli bir kullanıcıdan Tiki (rol) alma
|
||||
#[pallet::call_index(1)]
|
||||
#[pallet::weight(<T as crate::pallet::Config>::WeightInfo::revoke_tiki())]
|
||||
pub fn revoke_tiki(
|
||||
origin: OriginFor<T>,
|
||||
target: <T::Lookup as StaticLookup>::Source,
|
||||
tiki: Tiki,
|
||||
) -> DispatchResult {
|
||||
T::AdminOrigin::ensure_origin(origin)?;
|
||||
let target_account = T::Lookup::lookup(target)?;
|
||||
|
||||
Self::internal_revoke_role(&target_account, tiki)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Manually mint citizenship NFT (for testing/emergency)
|
||||
#[pallet::call_index(2)]
|
||||
#[pallet::weight(<T as crate::pallet::Config>::WeightInfo::grant_tiki())]
|
||||
pub fn force_mint_citizen_nft(
|
||||
origin: OriginFor<T>,
|
||||
dest: <T::Lookup as StaticLookup>::Source,
|
||||
) -> DispatchResult {
|
||||
T::AdminOrigin::ensure_origin(origin)?;
|
||||
let dest_account = T::Lookup::lookup(dest)?;
|
||||
|
||||
Self::mint_citizen_nft_for_user(&dest_account)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Grant role through election system (called from pezpallet-voting)
|
||||
#[pallet::call_index(3)]
|
||||
#[pallet::weight(<T as crate::pallet::Config>::WeightInfo::grant_tiki())]
|
||||
pub fn grant_elected_role(
|
||||
origin: OriginFor<T>,
|
||||
dest: <T::Lookup as StaticLookup>::Source,
|
||||
tiki: Tiki,
|
||||
) -> DispatchResult {
|
||||
T::AdminOrigin::ensure_origin(origin)?; // pezpallet-voting will call with Root origin
|
||||
let dest_account = T::Lookup::lookup(dest)?;
|
||||
|
||||
// Check if the role can be granted through election
|
||||
ensure!(
|
||||
Self::can_grant_role_type(&tiki, &RoleAssignmentType::Elected),
|
||||
Error::<T>::InvalidRoleAssignmentMethod
|
||||
);
|
||||
|
||||
Self::internal_grant_role(&dest_account, tiki)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Grant role through exam/test system
|
||||
#[pallet::call_index(4)]
|
||||
#[pallet::weight(<T as crate::pallet::Config>::WeightInfo::grant_tiki())]
|
||||
pub fn grant_earned_role(
|
||||
origin: OriginFor<T>,
|
||||
dest: <T::Lookup as StaticLookup>::Source,
|
||||
tiki: Tiki,
|
||||
) -> DispatchResult {
|
||||
T::AdminOrigin::ensure_origin(origin)?; // For now admin, later exam pallet
|
||||
let dest_account = T::Lookup::lookup(dest)?;
|
||||
|
||||
// Check if the role can be earned
|
||||
ensure!(
|
||||
Self::can_grant_role_type(&tiki, &RoleAssignmentType::Earned),
|
||||
Error::<T>::InvalidRoleAssignmentMethod
|
||||
);
|
||||
|
||||
Self::internal_grant_role(&dest_account, tiki)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Apply for citizenship after KYC completion
|
||||
#[pallet::call_index(5)]
|
||||
#[pallet::weight(<T as crate::pallet::Config>::WeightInfo::grant_tiki())]
|
||||
pub fn apply_for_citizenship(origin: OriginFor<T>) -> DispatchResult {
|
||||
let who = ensure_signed(origin)?;
|
||||
|
||||
// Check if user's KYC is approved
|
||||
let kyc_status = pezpallet_identity_kyc::Pallet::<T>::kyc_status_of(&who);
|
||||
ensure!(
|
||||
kyc_status == pezpallet_identity_kyc::types::KycLevel::Approved,
|
||||
Error::<T>::KycNotCompleted
|
||||
);
|
||||
|
||||
// Mint citizenship NFT
|
||||
Self::mint_citizen_nft_for_user(&who)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check NFT transfer for transfer blocking system
|
||||
#[pallet::call_index(6)]
|
||||
#[pallet::weight(<T as crate::pallet::Config>::WeightInfo::grant_tiki())]
|
||||
pub fn check_transfer_permission(
|
||||
_origin: OriginFor<T>,
|
||||
collection_id: T::CollectionId,
|
||||
item_id: u32,
|
||||
from: T::AccountId,
|
||||
to: T::AccountId,
|
||||
) -> DispatchResult {
|
||||
// Tiki NFT koleksiyonu ise transfer'e izin verme
|
||||
if collection_id == T::TikiCollectionId::get() {
|
||||
Self::deposit_event(Event::TransferBlocked { collection_id, item_id, from, to });
|
||||
return Err(DispatchError::Other("Citizen NFTs are non-transferable"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// Pallet's helper functions
|
||||
impl<T: Config> Pallet<T> {
|
||||
/// Checks newly KYC-completed users and mints citizenship NFT
|
||||
fn check_and_mint_citizen_nfts() {
|
||||
// Check all approved users in KYC pallet
|
||||
for (account, kyc_status) in pezpallet_identity_kyc::KycStatuses::<T>::iter() {
|
||||
// Check if KYC is approved
|
||||
if kyc_status == pezpallet_identity_kyc::types::KycLevel::Approved {
|
||||
// Check if citizenship NFT exists
|
||||
if Self::citizen_nft(&account).is_none() {
|
||||
// Mint NFT (log error but continue on failure)
|
||||
if Self::mint_citizen_nft_for_user(&account).is_err() {
|
||||
log::warn!("Failed to mint citizen NFT for account: {:?}", account);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Mints citizenship NFT for specific user
|
||||
pub fn mint_citizen_nft_for_user(user: &T::AccountId) -> DispatchResult {
|
||||
// Check if NFT already exists
|
||||
ensure!(Self::citizen_nft(user).is_none(), Error::<T>::CitizenNftAlreadyExists);
|
||||
|
||||
let collection_id = T::TikiCollectionId::get();
|
||||
let next_id_u32 = Self::next_item_id();
|
||||
|
||||
// Mint the NFT - use force_mint in benchmarks to bypass balance/origin requirements
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
pezpallet_nfts::Pallet::<T>::force_mint(
|
||||
T::RuntimeOrigin::from(pezframe_system::RawOrigin::Root),
|
||||
collection_id,
|
||||
next_id_u32,
|
||||
T::Lookup::unlookup(user.clone()),
|
||||
Default::default(),
|
||||
)?;
|
||||
|
||||
#[cfg(not(feature = "runtime-benchmarks"))]
|
||||
pezpallet_nfts::Pallet::<T>::force_mint(
|
||||
T::RuntimeOrigin::from(pezframe_system::RawOrigin::Root),
|
||||
collection_id,
|
||||
next_id_u32,
|
||||
T::Lookup::unlookup(user.clone()),
|
||||
Default::default(),
|
||||
)?;
|
||||
|
||||
// Make NFT non-transferable
|
||||
Self::lock_nft_transfer(&collection_id, &next_id_u32)?;
|
||||
|
||||
// Update storage
|
||||
CitizenNft::<T>::insert(user, next_id_u32);
|
||||
NextItemId::<T>::put(next_id_u32.saturating_add(1));
|
||||
|
||||
// Automatically add Welati role
|
||||
UserTikis::<T>::mutate(user, |tikis| {
|
||||
let _ = tikis.try_push(Tiki::Welati);
|
||||
});
|
||||
|
||||
// Set NFT metadata
|
||||
Self::update_nft_metadata(user)?;
|
||||
|
||||
Self::deposit_event(Event::CitizenNftMinted { who: user.clone(), nft_id: next_id_u32 });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Internal role granting function (to avoid code duplication)
|
||||
pub fn internal_grant_role(dest_account: &T::AccountId, tiki: Tiki) -> DispatchResult {
|
||||
// Check if citizenship NFT exists
|
||||
ensure!(Self::citizen_nft(dest_account).is_some(), Error::<T>::CitizenNftNotFound);
|
||||
|
||||
// If this role is unique (can belong to only one person), check
|
||||
if Self::is_unique_role(&tiki) {
|
||||
ensure!(Self::tiki_holder(tiki).is_none(), Error::<T>::RoleAlreadyTaken);
|
||||
}
|
||||
|
||||
// Check if user already has this role
|
||||
let user_tikis = Self::user_tikis(dest_account);
|
||||
ensure!(!user_tikis.contains(&tiki), Error::<T>::UserAlreadyHasRole);
|
||||
|
||||
// Add to user's Tiki list
|
||||
UserTikis::<T>::try_mutate(dest_account, |tikis| {
|
||||
tikis.try_push(tiki).map_err(|_| Error::<T>::ExceedsMaxRolesPerUser)
|
||||
})?;
|
||||
|
||||
// If unique role, also add to TikiHolder
|
||||
if Self::is_unique_role(&tiki) {
|
||||
TikiHolder::<T>::insert(tiki, dest_account);
|
||||
}
|
||||
|
||||
// Update NFT metadata
|
||||
Self::update_nft_metadata(dest_account)?;
|
||||
|
||||
Self::deposit_event(Event::TikiGranted { who: dest_account.clone(), tiki });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Internal role revocation function
|
||||
pub fn internal_revoke_role(target_account: &T::AccountId, tiki: Tiki) -> DispatchResult {
|
||||
// Check if user has this role
|
||||
let user_tikis = Self::user_tikis(target_account);
|
||||
let _position =
|
||||
user_tikis.iter().position(|&r| r == tiki).ok_or(Error::<T>::RoleNotAssigned)?;
|
||||
|
||||
// Welati role cannot be removed
|
||||
ensure!(tiki != Tiki::Welati, Error::<T>::RoleNotAssigned);
|
||||
|
||||
// Remove from user's Tiki list
|
||||
UserTikis::<T>::mutate(target_account, |tikis| {
|
||||
if let Some(pos) = tikis.iter().position(|&r| r == tiki) {
|
||||
tikis.swap_remove(pos);
|
||||
}
|
||||
});
|
||||
|
||||
// If unique role, also remove from TikiHolder
|
||||
if Self::is_unique_role(&tiki) {
|
||||
TikiHolder::<T>::remove(tiki);
|
||||
}
|
||||
|
||||
// Update NFT metadata
|
||||
Self::update_nft_metadata(target_account)?;
|
||||
|
||||
Self::deposit_event(Event::TikiRevoked { who: target_account.clone(), tiki });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Makes NFT non-transferable
|
||||
fn lock_nft_transfer(collection_id: &T::CollectionId, item_id: &u32) -> DispatchResult {
|
||||
// Mark NFT with lock attribute - use force_set_attribute in benchmarks to bypass
|
||||
// deposits
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
let _ = pezpallet_nfts::Pallet::<T>::force_set_attribute(
|
||||
T::RuntimeOrigin::from(pezframe_system::RawOrigin::Root),
|
||||
None,
|
||||
*collection_id,
|
||||
Some(*item_id),
|
||||
pezpallet_nfts::AttributeNamespace::Pallet,
|
||||
b"locked"
|
||||
.to_vec()
|
||||
.try_into()
|
||||
.map_err(|_| DispatchError::Other("Key too long"))?,
|
||||
b"true"
|
||||
.to_vec()
|
||||
.try_into()
|
||||
.map_err(|_| DispatchError::Other("Value too long"))?,
|
||||
);
|
||||
|
||||
#[cfg(not(feature = "runtime-benchmarks"))]
|
||||
let _ = pezpallet_nfts::Pallet::<T>::set_attribute(
|
||||
T::RuntimeOrigin::from(pezframe_system::RawOrigin::Root),
|
||||
*collection_id,
|
||||
Some(*item_id),
|
||||
pezpallet_nfts::AttributeNamespace::Pallet,
|
||||
b"locked"
|
||||
.to_vec()
|
||||
.try_into()
|
||||
.map_err(|_| DispatchError::Other("Key too long"))?,
|
||||
b"true"
|
||||
.to_vec()
|
||||
.try_into()
|
||||
.map_err(|_| DispatchError::Other("Value too long"))?,
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Updates NFT metadata based on user's roles
|
||||
fn update_nft_metadata(user: &T::AccountId) -> DispatchResult {
|
||||
let nft_id_u32 = Self::citizen_nft(user).ok_or(Error::<T>::CitizenNftNotFound)?;
|
||||
let collection_id = T::TikiCollectionId::get();
|
||||
let user_tikis = Self::user_tikis(user);
|
||||
|
||||
let total_score = Self::get_tiki_score(user);
|
||||
|
||||
// Short metadata - only basic information
|
||||
let metadata = format!(
|
||||
r#"{{"citizen":true,"roles":{},"score":{}}}"#,
|
||||
user_tikis.len(),
|
||||
total_score
|
||||
);
|
||||
|
||||
// Set metadata - log error but don't crash
|
||||
if pezpallet_nfts::Pallet::<T>::set_metadata(
|
||||
T::RuntimeOrigin::from(pezframe_system::RawOrigin::Root),
|
||||
collection_id,
|
||||
nft_id_u32,
|
||||
metadata
|
||||
.as_bytes()
|
||||
.to_vec()
|
||||
.try_into()
|
||||
.map_err(|_| DispatchError::Other("Metadata too long"))?,
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
log::warn!("Failed to set metadata for NFT: {:?}", nft_id_u32);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Checks if a specific role is unique (can belong to only one person)
|
||||
pub fn is_unique_role(tiki: &Tiki) -> bool {
|
||||
matches!(tiki, Tiki::Serok | Tiki::SerokiMeclise | Tiki::Xezinedar | Tiki::Balyoz)
|
||||
}
|
||||
|
||||
/// Returns the assignment type of a specific role
|
||||
pub fn get_role_assignment_type(tiki: &Tiki) -> RoleAssignmentType {
|
||||
match tiki {
|
||||
// Automatic roles
|
||||
Tiki::Welati => RoleAssignmentType::Automatic,
|
||||
|
||||
// Elected roles
|
||||
Tiki::Parlementer | Tiki::SerokiMeclise | Tiki::Serok =>
|
||||
RoleAssignmentType::Elected,
|
||||
|
||||
// Earned roles (automatically given by pezpallet-referral)
|
||||
Tiki::Axa |
|
||||
Tiki::Mamoste |
|
||||
Tiki::Rewsenbîr |
|
||||
Tiki::SerokêKomele |
|
||||
Tiki::ModeratorêCivakê => RoleAssignmentType::Earned,
|
||||
|
||||
// Appointed roles (default)
|
||||
_ => RoleAssignmentType::Appointed,
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks the granting method of a specific role
|
||||
pub fn can_grant_role_type(tiki: &Tiki, assignment_type: &RoleAssignmentType) -> bool {
|
||||
let required_type = Self::get_role_assignment_type(tiki);
|
||||
match (&required_type, assignment_type) {
|
||||
// Automatic roles can only be given by the system
|
||||
(RoleAssignmentType::Automatic, RoleAssignmentType::Automatic) => true,
|
||||
// Appointed roles can be given by admin
|
||||
(RoleAssignmentType::Appointed, RoleAssignmentType::Appointed) => true,
|
||||
// Elected roles can be given by election system
|
||||
(RoleAssignmentType::Elected, RoleAssignmentType::Elected) => true,
|
||||
// Earned roles can be given by exam/test system
|
||||
(RoleAssignmentType::Earned, RoleAssignmentType::Earned) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// KYC sonrası otomatik Welati rolü verme
|
||||
pub fn auto_grant_citizenship(account: &T::AccountId) -> DispatchResult {
|
||||
// KYC kontrolü
|
||||
let kyc_status = pezpallet_identity_kyc::Pallet::<T>::kyc_status_of(account);
|
||||
if kyc_status == pezpallet_identity_kyc::types::KycLevel::Approved {
|
||||
// Vatandaşlık NFT'si yoksa bas
|
||||
if Self::citizen_nft(account).is_none() {
|
||||
Self::mint_citizen_nft_for_user(account)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Kullanıcının belirli bir Tiki'ye sahip olup olmadığını kontrol eder
|
||||
pub fn has_tiki(who: &T::AccountId, tiki: &Tiki) -> bool {
|
||||
Self::user_tikis(who).contains(tiki)
|
||||
}
|
||||
|
||||
/// Kullanıcının vatandaş olup olmadığını kontrol eder
|
||||
pub fn is_citizen(who: &T::AccountId) -> bool {
|
||||
Self::citizen_nft(who).is_some()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Diğer paletlerin, bu paletten Tiki puanlarını sorgulaması için kullanılacak trait
|
||||
pub trait TikiScoreProvider<AccountId> {
|
||||
fn get_tiki_score(who: &AccountId) -> u32;
|
||||
}
|
||||
|
||||
/// Diğer paletlerin, Tiki sahipliğini sorgulaması için kullanılacak trait
|
||||
pub trait TikiProvider<AccountId> {
|
||||
fn has_tiki(who: &AccountId, tiki: &Tiki) -> bool;
|
||||
fn get_user_tikis(who: &AccountId) -> Vec<Tiki>;
|
||||
fn is_citizen(who: &AccountId) -> bool;
|
||||
}
|
||||
|
||||
/// Trait implementasyonları
|
||||
impl<T: Config> TikiScoreProvider<T::AccountId> for Pallet<T> {
|
||||
fn get_tiki_score(who: &T::AccountId) -> u32 {
|
||||
let tikis = Self::user_tikis(who);
|
||||
tikis.iter().map(Self::get_bonus_for_tiki).sum()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> TikiProvider<T::AccountId> for Pallet<T> {
|
||||
fn has_tiki(who: &T::AccountId, tiki: &Tiki) -> bool {
|
||||
Self::has_tiki(who, tiki)
|
||||
}
|
||||
|
||||
fn get_user_tikis(who: &T::AccountId) -> Vec<Tiki> {
|
||||
Self::user_tikis(who).into_inner()
|
||||
}
|
||||
|
||||
fn is_citizen(who: &T::AccountId) -> bool {
|
||||
Self::is_citizen(who)
|
||||
}
|
||||
}
|
||||
|
||||
// Puanlama mantığını ayrı bir impl bloğunda tutarak kodu daha düzenli hale getiriyoruz.
|
||||
impl<T: Config> Pallet<T> {
|
||||
/// Belirli bir Tiki'nin Trust Puanı'na olan katkısını döndürür.
|
||||
pub fn get_bonus_for_tiki(tiki: &Tiki) -> u32 {
|
||||
match tiki {
|
||||
// Anayasa v5.0'da Belirlenen Özel Puanlar
|
||||
Tiki::Axa => 250,
|
||||
Tiki::RêveberêProjeyê => 250,
|
||||
Tiki::ModeratorêCivakê => 200,
|
||||
Tiki::SerokêKomele => 100,
|
||||
Tiki::Mela => 50,
|
||||
Tiki::Feqî => 50,
|
||||
|
||||
// Hiyerarşik Devlet Puanları
|
||||
// Yargı
|
||||
Tiki::EndameDiwane => 175,
|
||||
Tiki::Dadger => 150,
|
||||
Tiki::Dozger => 120,
|
||||
Tiki::Hiquqnas => 75,
|
||||
// Yürütme
|
||||
Tiki::Serok => 200,
|
||||
Tiki::Wezir => 100,
|
||||
Tiki::SerokWeziran => 125,
|
||||
Tiki::WezireDarayiye => 100,
|
||||
Tiki::WezireParez => 100,
|
||||
Tiki::WezireDad => 100,
|
||||
Tiki::WezireBelaw => 100,
|
||||
Tiki::WezireTend => 100,
|
||||
Tiki::WezireAva => 100,
|
||||
Tiki::WezireCand => 100,
|
||||
|
||||
// Yasama
|
||||
Tiki::SerokiMeclise => 150,
|
||||
Tiki::Parlementer => 100,
|
||||
|
||||
// Atanmış Üst Düzey Memurlar
|
||||
Tiki::Xezinedar => 100,
|
||||
Tiki::PisporêEwlehiyaSîber => 100,
|
||||
Tiki::Mufetîs => 90,
|
||||
Tiki::Balyoz => 80,
|
||||
Tiki::Berdevk => 70,
|
||||
|
||||
// Diğer Memurlar ve Uzmanlar
|
||||
Tiki::Mamoste => 70,
|
||||
Tiki::OperatorêTorê => 60,
|
||||
Tiki::Noter => 50,
|
||||
Tiki::Bacgir => 50,
|
||||
Tiki::Perwerdekar => 40,
|
||||
Tiki::Rewsenbîr => 40,
|
||||
Tiki::GerinendeyeCavkaniye => 40,
|
||||
Tiki::GerinendeyeDaneye => 40,
|
||||
Tiki::KalîteKontrolker => 30,
|
||||
Tiki::Navbeynkar => 30,
|
||||
Tiki::Hekem => 30,
|
||||
Tiki::Qeydkar => 25,
|
||||
Tiki::ParêzvaneÇandî => 25,
|
||||
Tiki::Sêwirmend => 20,
|
||||
Tiki::Bazargan => 60, // Yeni eklenen ekonomik rol
|
||||
|
||||
// Temel Vatandaşlık ve Diğerleri
|
||||
Tiki::Welati => 10,
|
||||
// Yukarıdaki listede olmayan diğer tüm roller 5 puan alır.
|
||||
_ => 5,
|
||||
}
|
||||
}
|
||||
}
|
||||
// CitizenNftProvider trait implementation for pezpallet-identity-kyc integration
|
||||
impl<T: Config> pezpallet_identity_kyc::types::CitizenNftProvider<T::AccountId> for Pallet<T> {
|
||||
fn mint_citizen_nft(who: &T::AccountId) -> pezsp_runtime::DispatchResult {
|
||||
Self::mint_citizen_nft_for_user(who)
|
||||
}
|
||||
|
||||
fn mint_citizen_nft_confirmed(who: &T::AccountId) -> pezsp_runtime::DispatchResult {
|
||||
// For self-confirmation, we use the same mint function with force_mint
|
||||
Self::mint_citizen_nft_for_user(who)
|
||||
}
|
||||
|
||||
fn burn_citizen_nft(who: &T::AccountId) -> pezsp_runtime::DispatchResult {
|
||||
use pezframe_support::traits::Get;
|
||||
// Get the citizen NFT item ID
|
||||
let item_id = Self::citizen_nft(who).ok_or(Error::<T>::CitizenNftNotFound)?;
|
||||
let collection_id = T::TikiCollectionId::get();
|
||||
|
||||
// Burn the NFT using pezpallet_nfts burn function
|
||||
pezpallet_nfts::Pallet::<T>::burn(
|
||||
T::RuntimeOrigin::from(pezframe_system::RawOrigin::Signed(who.clone())),
|
||||
collection_id,
|
||||
item_id,
|
||||
)?;
|
||||
|
||||
// Remove from our storage
|
||||
CitizenNft::<T>::remove(who);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
//! Storage migrations for pezpallet-tiki
|
||||
|
||||
use super::*;
|
||||
use pezframe_support::{
|
||||
traits::{Get, GetStorageVersion, OnRuntimeUpgrade, StorageVersion},
|
||||
weights::Weight,
|
||||
};
|
||||
use pezsp_std::marker::PhantomData;
|
||||
|
||||
/// Current storage version
|
||||
pub const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
|
||||
|
||||
/// Migration from v0 to v1
|
||||
/// This is a template migration that can be customized based on actual storage changes
|
||||
pub mod v1 {
|
||||
use super::*;
|
||||
|
||||
pub struct MigrateToV1<T>(PhantomData<T>);
|
||||
|
||||
impl<T: Config> OnRuntimeUpgrade for MigrateToV1<T> {
|
||||
fn on_runtime_upgrade() -> Weight {
|
||||
let current = Pallet::<T>::on_chain_storage_version();
|
||||
|
||||
log::info!(
|
||||
"🔄 Running migration for pezpallet-tiki from {:?} to {:?}",
|
||||
current,
|
||||
STORAGE_VERSION
|
||||
);
|
||||
|
||||
if current == StorageVersion::new(0) {
|
||||
// Perform migration logic here
|
||||
// Example: Iterate over storage and transform data
|
||||
|
||||
let migrated = 0u64;
|
||||
let mut weight = Weight::zero();
|
||||
|
||||
// Example: Migrate CitizenNft storage if format changed
|
||||
// for (account, nft_id) in CitizenNft::<T>::iter() {
|
||||
// // Transform data if needed
|
||||
// migrated += 1;
|
||||
// }
|
||||
|
||||
// Update storage version
|
||||
STORAGE_VERSION.put::<Pallet<T>>();
|
||||
|
||||
log::info!("✅ Migrated {} entries in pezpallet-tiki", migrated);
|
||||
|
||||
// Return weight used
|
||||
// Reads: migrated items + version read
|
||||
// Writes: migrated items + version write
|
||||
weight = weight
|
||||
.saturating_add(T::DbWeight::get().reads_writes(migrated + 1, migrated + 1));
|
||||
|
||||
weight
|
||||
} else {
|
||||
log::info!("👌 pezpallet-tiki migration not needed, current version is {:?}", current);
|
||||
T::DbWeight::get().reads(1)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn pre_upgrade() -> Result<pezsp_std::vec::Vec<u8>, pezsp_runtime::TryRuntimeError> {
|
||||
use codec::Encode;
|
||||
|
||||
let current = Pallet::<T>::on_chain_storage_version();
|
||||
|
||||
log::info!("🔍 Pre-upgrade check for pezpallet-tiki");
|
||||
log::info!(" Current version: {:?}", current);
|
||||
|
||||
// Encode current storage counts for verification
|
||||
let citizen_count = CitizenNft::<T>::iter().count() as u32;
|
||||
let user_tikis_count = UserTikis::<T>::iter().count() as u32;
|
||||
let tiki_holder_count = TikiHolder::<T>::iter().count() as u32;
|
||||
|
||||
log::info!(" CitizenNft entries: {}", citizen_count);
|
||||
log::info!(" UserTikis entries: {}", user_tikis_count);
|
||||
log::info!(" TikiHolder entries: {}", tiki_holder_count);
|
||||
|
||||
Ok((citizen_count, user_tikis_count, tiki_holder_count).encode())
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn post_upgrade(state: pezsp_std::vec::Vec<u8>) -> Result<(), pezsp_runtime::TryRuntimeError> {
|
||||
use codec::Decode;
|
||||
|
||||
let (pre_citizen_count, pre_user_tikis_count, pre_tiki_holder_count): (u32, u32, u32) =
|
||||
Decode::decode(&mut &state[..]).map_err(|_| "Failed to decode pre-upgrade state")?;
|
||||
|
||||
log::info!("🔍 Post-upgrade check for pezpallet-tiki");
|
||||
|
||||
// Verify storage version was updated
|
||||
let current_version = Pallet::<T>::on_chain_storage_version();
|
||||
assert_eq!(current_version, STORAGE_VERSION, "Storage version not updated correctly");
|
||||
log::info!("✅ Storage version updated to {:?}", current_version);
|
||||
|
||||
// Verify storage counts (should be same or more, never less)
|
||||
let post_citizen_count = CitizenNft::<T>::iter().count() as u32;
|
||||
let post_user_tikis_count = UserTikis::<T>::iter().count() as u32;
|
||||
let post_tiki_holder_count = TikiHolder::<T>::iter().count() as u32;
|
||||
|
||||
log::info!(" CitizenNft entries: {} -> {}", pre_citizen_count, post_citizen_count);
|
||||
log::info!(
|
||||
" UserTikis entries: {} -> {}",
|
||||
pre_user_tikis_count,
|
||||
post_user_tikis_count
|
||||
);
|
||||
log::info!(
|
||||
" TikiHolder entries: {} -> {}",
|
||||
pre_tiki_holder_count,
|
||||
post_tiki_holder_count
|
||||
);
|
||||
|
||||
assert!(
|
||||
post_citizen_count >= pre_citizen_count,
|
||||
"CitizenNft entries decreased during migration"
|
||||
);
|
||||
assert!(
|
||||
post_user_tikis_count >= pre_user_tikis_count,
|
||||
"UserTikis entries decreased during migration"
|
||||
);
|
||||
assert!(
|
||||
post_tiki_holder_count >= pre_tiki_holder_count,
|
||||
"TikiHolder entries decreased during migration"
|
||||
);
|
||||
|
||||
log::info!("✅ Post-upgrade checks passed for pezpallet-tiki");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Example migration for future version changes
|
||||
/// This demonstrates how to handle storage item renames or format changes
|
||||
pub mod v2 {
|
||||
use super::*;
|
||||
|
||||
/// Example: Migration when storage format changes
|
||||
pub struct MigrateToV2<T>(PhantomData<T>);
|
||||
|
||||
impl<T: Config> OnRuntimeUpgrade for MigrateToV2<T> {
|
||||
fn on_runtime_upgrade() -> Weight {
|
||||
let current = Pallet::<T>::on_chain_storage_version();
|
||||
|
||||
if current < StorageVersion::new(2) {
|
||||
log::info!("🔄 Running migration for pezpallet-tiki to v2");
|
||||
|
||||
// Example migration logic
|
||||
// 1. Create new storage with modified format
|
||||
// 2. Migrate data from old storage to new
|
||||
// 3. Remove old storage
|
||||
// 4. Update version
|
||||
|
||||
// For now, this is just a template
|
||||
STORAGE_VERSION.put::<Pallet<T>>();
|
||||
|
||||
log::info!("✅ Completed migration to pezpallet-tiki v2");
|
||||
|
||||
T::DbWeight::get().reads_writes(1, 1)
|
||||
} else {
|
||||
log::info!("👌 pezpallet-tiki v2 migration not needed");
|
||||
T::DbWeight::get().reads(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::mock::{new_test_ext, Test};
|
||||
use pezframe_support::traits::OnRuntimeUpgrade;
|
||||
|
||||
#[test]
|
||||
fn test_migration_v1() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// Set initial storage version to 0
|
||||
StorageVersion::new(0).put::<Pallet<Test>>();
|
||||
|
||||
// Run migration
|
||||
let weight = v1::MigrateToV1::<Test>::on_runtime_upgrade();
|
||||
|
||||
// Verify version was updated
|
||||
assert_eq!(Pallet::<Test>::on_chain_storage_version(), STORAGE_VERSION);
|
||||
|
||||
// Verify weight is non-zero
|
||||
assert!(weight != Weight::zero());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_migration_idempotent() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// Set current version
|
||||
STORAGE_VERSION.put::<Pallet<Test>>();
|
||||
|
||||
// Run migration again
|
||||
let weight = v1::MigrateToV1::<Test>::on_runtime_upgrade();
|
||||
|
||||
// Should be a no-op
|
||||
assert_eq!(weight, pezframe_support::weights::constants::RocksDbWeight::get().reads(1));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
use crate as pezpallet_tiki;
|
||||
use crate::Tiki as TikiEnum;
|
||||
use pezframe_support::{
|
||||
assert_ok, construct_runtime, parameter_types,
|
||||
traits::{AsEnsureOriginWithArg, ConstU128, ConstU16, ConstU32, ConstU64},
|
||||
};
|
||||
use pezsp_core::H256;
|
||||
use pezsp_runtime::{
|
||||
traits::{BlakeTwo256, IdentityLookup},
|
||||
BuildStorage,
|
||||
};
|
||||
|
||||
type Block = pezframe_system::mocking::MockBlock<Test>;
|
||||
pub type AccountId = u64;
|
||||
pub type Balance = u128;
|
||||
|
||||
// Runtime'ı oluştur - Identity ve IdentityKyc pallet'lerini de ekle
|
||||
construct_runtime!(
|
||||
pub enum Test
|
||||
{
|
||||
System: pezframe_system::{Pallet, Call, Config<T>, Storage, Event<T>},
|
||||
Balances: pezpallet_balances::{Pallet, Call, Storage, Event<T>},
|
||||
Identity: pezpallet_identity::{Pallet, Call, Storage, Event<T>},
|
||||
IdentityKyc: pezpallet_identity_kyc::{Pallet, Call, Storage, Event<T>},
|
||||
Nfts: pezpallet_nfts::{Pallet, Call, Storage, Event<T>},
|
||||
Tiki: pezpallet_tiki::{Pallet, Call, Storage, Event<T>},
|
||||
}
|
||||
);
|
||||
|
||||
impl pezframe_system::Config for Test {
|
||||
type BaseCallFilter = pezframe_support::traits::Everything;
|
||||
type BlockWeights = ();
|
||||
type BlockLength = ();
|
||||
type DbWeight = pezframe_support::weights::constants::RocksDbWeight;
|
||||
type RuntimeOrigin = RuntimeOrigin;
|
||||
type RuntimeCall = RuntimeCall;
|
||||
type Nonce = u64;
|
||||
type Hash = H256;
|
||||
type Hashing = BlakeTwo256;
|
||||
type AccountId = AccountId;
|
||||
type Lookup = IdentityLookup<Self::AccountId>;
|
||||
type Block = Block;
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type BlockHashCount = ConstU64<250>;
|
||||
type Version = ();
|
||||
type PalletInfo = PalletInfo;
|
||||
type AccountData = pezpallet_balances::AccountData<Balance>;
|
||||
type OnNewAccount = ();
|
||||
type OnKilledAccount = ();
|
||||
type SystemWeightInfo = ();
|
||||
type SS58Prefix = ConstU16<42>;
|
||||
type OnSetCode = ();
|
||||
type MaxConsumers = ConstU32<16>;
|
||||
type RuntimeTask = ();
|
||||
type SingleBlockMigrations = ();
|
||||
type MultiBlockMigrator = ();
|
||||
type PreInherents = ();
|
||||
type PostInherents = ();
|
||||
type PostTransactions = (); // Eksik olan trait
|
||||
type ExtensionsWeightInfo = ();
|
||||
}
|
||||
|
||||
impl pezpallet_balances::Config for Test {
|
||||
type Balance = Balance;
|
||||
type DustRemoval = ();
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type ExistentialDeposit = ConstU128<1>;
|
||||
type AccountStore = System;
|
||||
type WeightInfo = ();
|
||||
type MaxLocks = ConstU32<50>;
|
||||
type MaxReserves = ();
|
||||
type ReserveIdentifier = [u8; 8];
|
||||
type FreezeIdentifier = ();
|
||||
type MaxFreezes = ();
|
||||
type RuntimeHoldReason = ();
|
||||
type RuntimeFreezeReason = ();
|
||||
type DoneSlashHandler = ();
|
||||
}
|
||||
|
||||
// pezpallet_identity::Config implementasyonu
|
||||
parameter_types! {
|
||||
pub const BasicDeposit: Balance = 1000;
|
||||
pub const ByteDeposit: Balance = 10;
|
||||
pub const SubAccountDeposit: Balance = 100;
|
||||
pub const MaxSubAccounts: u32 = 10;
|
||||
pub const MaxRegistrars: u32 = 10;
|
||||
pub const UsernameDeposit: Balance = 100;
|
||||
pub const PendingUsernameExpiration: u64 = 100;
|
||||
pub const UsernameGracePeriod: u64 = 50;
|
||||
pub const MaxSuffixLength: u32 = 10;
|
||||
pub const MaxUsernameLength: u32 = 32;
|
||||
}
|
||||
|
||||
impl pezpallet_identity::Config for Test {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type Currency = Balances;
|
||||
type BasicDeposit = BasicDeposit;
|
||||
type ByteDeposit = ByteDeposit;
|
||||
type SubAccountDeposit = SubAccountDeposit;
|
||||
type MaxSubAccounts = MaxSubAccounts;
|
||||
type IdentityInformation = pezpallet_identity::legacy::IdentityInfo<MaxAdditionalFields>;
|
||||
type MaxRegistrars = MaxRegistrars;
|
||||
type Slashed = ();
|
||||
type ForceOrigin = pezframe_system::EnsureRoot<AccountId>;
|
||||
type RegistrarOrigin = pezframe_system::EnsureRoot<AccountId>;
|
||||
type WeightInfo = ();
|
||||
type OffchainSignature = pezsp_runtime::testing::TestSignature;
|
||||
type SigningPublicKey =
|
||||
<pezsp_runtime::testing::TestSignature as pezsp_runtime::traits::Verify>::Signer;
|
||||
type UsernameAuthorityOrigin = pezframe_system::EnsureRoot<AccountId>;
|
||||
type UsernameDeposit = UsernameDeposit;
|
||||
type PendingUsernameExpiration = PendingUsernameExpiration;
|
||||
type UsernameGracePeriod = UsernameGracePeriod;
|
||||
type MaxSuffixLength = MaxSuffixLength;
|
||||
type MaxUsernameLength = MaxUsernameLength;
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const MaxAdditionalFields: u32 = 10;
|
||||
}
|
||||
|
||||
// pezpallet_identity_kyc::Config parameters
|
||||
parameter_types! {
|
||||
pub const KycApplicationDepositAmount: Balance = 100;
|
||||
pub const MaxCidLength: u32 = 100;
|
||||
}
|
||||
|
||||
// Mock implementation for OnKycApproved hook (updated for new trait signature)
|
||||
pub struct MockOnKycApproved;
|
||||
impl pezpallet_identity_kyc::types::OnKycApproved<AccountId> for MockOnKycApproved {
|
||||
fn on_kyc_approved(_who: &AccountId, _referrer: &AccountId) {
|
||||
// No-op for tests
|
||||
}
|
||||
}
|
||||
|
||||
// Mock implementation for OnCitizenshipRevoked hook
|
||||
pub struct MockOnCitizenshipRevoked;
|
||||
impl pezpallet_identity_kyc::types::OnCitizenshipRevoked<AccountId> for MockOnCitizenshipRevoked {
|
||||
fn on_citizenship_revoked(_who: &AccountId) {
|
||||
// No-op for tests
|
||||
}
|
||||
}
|
||||
|
||||
// Mock implementation for CitizenNftProvider
|
||||
pub struct MockCitizenNftProvider;
|
||||
impl pezpallet_identity_kyc::types::CitizenNftProvider<AccountId> for MockCitizenNftProvider {
|
||||
fn mint_citizen_nft(_who: &AccountId) -> pezsp_runtime::DispatchResult {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn mint_citizen_nft_confirmed(_who: &AccountId) -> pezsp_runtime::DispatchResult {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn burn_citizen_nft(_who: &AccountId) -> pezsp_runtime::DispatchResult {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl pezpallet_identity_kyc::Config for Test {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type Currency = Balances;
|
||||
type WeightInfo = ();
|
||||
type GovernanceOrigin = pezframe_system::EnsureRoot<AccountId>;
|
||||
type KycApplicationDeposit = KycApplicationDepositAmount;
|
||||
type MaxStringLength = ConstU32<50>;
|
||||
type MaxCidLength = MaxCidLength;
|
||||
type OnKycApproved = MockOnKycApproved;
|
||||
type OnCitizenshipRevoked = MockOnCitizenshipRevoked;
|
||||
type CitizenNftProvider = MockCitizenNftProvider;
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub Features: pezpallet_nfts::PalletFeatures = pezpallet_nfts::PalletFeatures::default();
|
||||
}
|
||||
|
||||
impl pezpallet_nfts::Config for Test {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type CollectionId = u32;
|
||||
type ItemId = u32;
|
||||
type Currency = Balances;
|
||||
type ForceOrigin = pezframe_system::EnsureRoot<AccountId>;
|
||||
type CreateOrigin = AsEnsureOriginWithArg<pezframe_system::EnsureSigned<AccountId>>;
|
||||
type Locker = ();
|
||||
type CollectionDeposit = ConstU128<0>;
|
||||
type ItemDeposit = ConstU128<0>;
|
||||
type MetadataDepositBase = ConstU128<0>;
|
||||
type AttributeDepositBase = ConstU128<0>;
|
||||
type DepositPerByte = ConstU128<0>;
|
||||
type StringLimit = ConstU32<256>;
|
||||
type KeyLimit = ConstU32<64>;
|
||||
type ValueLimit = ConstU32<256>;
|
||||
type ApprovalsLimit = ConstU32<10>;
|
||||
type ItemAttributesApprovalsLimit = ConstU32<20>;
|
||||
type MaxTips = ConstU32<10>;
|
||||
type MaxDeadlineDuration = ConstU64<10000>;
|
||||
type MaxAttributesPerCall = ConstU32<10>;
|
||||
type Features = Features;
|
||||
type OffchainSignature = pezsp_runtime::testing::TestSignature;
|
||||
type OffchainPublic = <Self::OffchainSignature as pezsp_runtime::traits::Verify>::Signer;
|
||||
type WeightInfo = ();
|
||||
type BlockNumberProvider = System;
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
type Helper = ();
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const TikiCollectionId: u32 = 0;
|
||||
pub const MaxTikisPerUser: u32 = 100;
|
||||
}
|
||||
|
||||
impl crate::Config for Test {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type AdminOrigin = pezframe_system::EnsureRoot<AccountId>;
|
||||
type WeightInfo = ();
|
||||
type TikiCollectionId = TikiCollectionId;
|
||||
type MaxTikisPerUser = MaxTikisPerUser;
|
||||
type Tiki = TikiEnum;
|
||||
}
|
||||
|
||||
// Helper functions for tests
|
||||
// Updated for trustless model - directly sets KYC status and hash
|
||||
pub fn setup_kyc_for_user(account: AccountId) {
|
||||
// Give balance to user
|
||||
let _ = Balances::force_set_balance(RuntimeOrigin::root(), account, 10000);
|
||||
|
||||
// Directly set KYC status to Approved (for test purposes)
|
||||
// In real runtime this would go through apply_for_citizenship -> approve_referral ->
|
||||
// confirm_citizenship
|
||||
pezpallet_identity_kyc::KycStatuses::<Test>::insert(
|
||||
account,
|
||||
pezpallet_identity_kyc::types::KycLevel::Approved,
|
||||
);
|
||||
|
||||
// Set identity hash
|
||||
pezpallet_identity_kyc::IdentityHashes::<Test>::insert(
|
||||
account,
|
||||
pezsp_core::H256::from_low_u64_be(account),
|
||||
);
|
||||
}
|
||||
|
||||
// Legacy function - kept for backwards compatibility
|
||||
pub fn setup_identity_for_user(account: AccountId) {
|
||||
setup_kyc_for_user(account);
|
||||
}
|
||||
|
||||
pub fn advance_blocks(blocks: u64) {
|
||||
for _i in 0..blocks {
|
||||
let current_block = System::block_number();
|
||||
System::set_block_number(current_block + 1);
|
||||
// Trigger hooks for the new block
|
||||
<pezpallet_tiki::Pallet<Test> as pezframe_support::traits::Hooks<u64>>::on_initialize(
|
||||
current_block + 1,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_test_ext() -> pezsp_io::TestExternalities {
|
||||
let mut t = pezframe_system::GenesisConfig::<Test>::default().build_storage().unwrap();
|
||||
|
||||
pezpallet_balances::GenesisConfig::<Test> {
|
||||
balances: vec![(1, 10000), (2, 10000), (3, 10000), (4, 10000), (5, 10000)],
|
||||
dev_accounts: Default::default(),
|
||||
}
|
||||
.assimilate_storage(&mut t)
|
||||
.unwrap();
|
||||
|
||||
let mut ext = pezsp_io::TestExternalities::new(t);
|
||||
ext.execute_with(|| {
|
||||
System::set_block_number(1);
|
||||
|
||||
// Tiki koleksiyonunu oluştur - mint permissions ile
|
||||
assert_ok!(Nfts::force_create(
|
||||
RuntimeOrigin::root(),
|
||||
1, // owner
|
||||
pezpallet_nfts::CollectionConfig {
|
||||
settings: pezpallet_nfts::CollectionSettings::all_enabled(),
|
||||
max_supply: None,
|
||||
mint_settings: pezpallet_nfts::MintSettings {
|
||||
mint_type: pezpallet_nfts::MintType::Public,
|
||||
price: None,
|
||||
start_block: None,
|
||||
end_block: None,
|
||||
default_item_settings: pezpallet_nfts::ItemSettings::all_enabled(),
|
||||
},
|
||||
}
|
||||
));
|
||||
});
|
||||
ext
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,298 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
|
||||
//! Autogenerated weights for `pezpallet_tiki`
|
||||
//!
|
||||
//! THIS FILE WAS AUTO-GENERATED USING THE BIZINIKIWI BENCHMARK CLI VERSION 32.0.0
|
||||
//! DATE: 2025-12-08, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
|
||||
//! WORST CASE MAP SIZE: `1000000`
|
||||
//! HOSTNAME: `MamostePC`, CPU: `11th Gen Intel(R) Core(TM) i9-11950H @ 2.60GHz`
|
||||
//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: `1024`
|
||||
|
||||
// Executed Command:
|
||||
// ./target/release/frame-omni-bencher
|
||||
// v1
|
||||
// benchmark
|
||||
// pallet
|
||||
// --runtime
|
||||
// target/release/wbuild/people-pezkuwichain-runtime/people_pezkuwichain_runtime.compact.compressed.wasm
|
||||
// --pallets
|
||||
// pezpallet_tiki
|
||||
// -e
|
||||
// all
|
||||
// --steps
|
||||
// 50
|
||||
// --repeat
|
||||
// 20
|
||||
// --output
|
||||
// pezcumulus/teyrchains/pallets/tiki/src/weights.rs
|
||||
// --template
|
||||
// bizinikiwi/.maintain/frame-weight-template.hbs
|
||||
|
||||
#![cfg_attr(rustfmt, rustfmt_skip)]
|
||||
#![allow(unused_parens)]
|
||||
#![allow(unused_imports)]
|
||||
#![allow(missing_docs)]
|
||||
#![allow(dead_code)]
|
||||
|
||||
use pezframe_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}};
|
||||
use core::marker::PhantomData;
|
||||
|
||||
/// Weight functions needed for `pezpallet_tiki`.
|
||||
pub trait WeightInfo {
|
||||
fn grant_tiki() -> Weight;
|
||||
fn revoke_tiki() -> Weight;
|
||||
fn force_mint_citizen_nft() -> Weight;
|
||||
fn grant_earned_role() -> Weight;
|
||||
fn grant_elected_role() -> Weight;
|
||||
}
|
||||
|
||||
/// Weights for `pezpallet_tiki` using the Bizinikiwi node and recommended hardware.
|
||||
pub struct BizinikiwiWeight<T>(PhantomData<T>);
|
||||
impl<T: pezframe_system::Config> WeightInfo for BizinikiwiWeight<T> {
|
||||
/// Storage: `Tiki::CitizenNft` (r:1 w:0)
|
||||
/// Proof: `Tiki::CitizenNft` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Tiki::UserTikis` (r:1 w:1)
|
||||
/// Proof: `Tiki::UserTikis` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Nfts::Collection` (r:1 w:1)
|
||||
/// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Nfts::ItemConfigOf` (r:1 w:0)
|
||||
/// Proof: `Nfts::ItemConfigOf` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Nfts::CollectionConfigOf` (r:1 w:0)
|
||||
/// Proof: `Nfts::CollectionConfigOf` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Nfts::ItemMetadataOf` (r:1 w:1)
|
||||
/// Proof: `Nfts::ItemMetadataOf` (`max_values`: None, `max_size`: Some(347), added: 2822, mode: `MaxEncodedLen`)
|
||||
fn grant_tiki() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `848`
|
||||
// Estimated: `3812`
|
||||
// Minimum execution time: 50_144_000 picoseconds.
|
||||
Weight::from_parts(51_105_000, 3812)
|
||||
.saturating_add(T::DbWeight::get().reads(6_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(3_u64))
|
||||
}
|
||||
/// Storage: `Tiki::UserTikis` (r:1 w:1)
|
||||
/// Proof: `Tiki::UserTikis` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Tiki::CitizenNft` (r:1 w:0)
|
||||
/// Proof: `Tiki::CitizenNft` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Nfts::Collection` (r:1 w:1)
|
||||
/// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Nfts::ItemConfigOf` (r:1 w:0)
|
||||
/// Proof: `Nfts::ItemConfigOf` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Nfts::CollectionConfigOf` (r:1 w:0)
|
||||
/// Proof: `Nfts::CollectionConfigOf` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Nfts::ItemMetadataOf` (r:1 w:1)
|
||||
/// Proof: `Nfts::ItemMetadataOf` (`max_values`: None, `max_size`: Some(347), added: 2822, mode: `MaxEncodedLen`)
|
||||
fn revoke_tiki() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `850`
|
||||
// Estimated: `3812`
|
||||
// Minimum execution time: 48_006_000 picoseconds.
|
||||
Weight::from_parts(49_738_000, 3812)
|
||||
.saturating_add(T::DbWeight::get().reads(6_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(3_u64))
|
||||
}
|
||||
/// Storage: `Tiki::CitizenNft` (r:1 w:1)
|
||||
/// Proof: `Tiki::CitizenNft` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Tiki::NextItemId` (r:1 w:1)
|
||||
/// Proof: `Tiki::NextItemId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Nfts::Item` (r:1 w:1)
|
||||
/// Proof: `Nfts::Item` (`max_values`: None, `max_size`: Some(861), added: 3336, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Nfts::Collection` (r:1 w:1)
|
||||
/// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Nfts::CollectionConfigOf` (r:1 w:0)
|
||||
/// Proof: `Nfts::CollectionConfigOf` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Nfts::ItemConfigOf` (r:1 w:1)
|
||||
/// Proof: `Nfts::ItemConfigOf` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Nfts::Attribute` (r:1 w:1)
|
||||
/// Proof: `Nfts::Attribute` (`max_values`: None, `max_size`: Some(479), added: 2954, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Tiki::UserTikis` (r:1 w:1)
|
||||
/// Proof: `Tiki::UserTikis` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Nfts::ItemMetadataOf` (r:1 w:1)
|
||||
/// Proof: `Nfts::ItemMetadataOf` (`max_values`: None, `max_size`: Some(347), added: 2822, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Nfts::Account` (r:0 w:1)
|
||||
/// Proof: `Nfts::Account` (`max_values`: None, `max_size`: Some(88), added: 2563, mode: `MaxEncodedLen`)
|
||||
fn force_mint_citizen_nft() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `439`
|
||||
// Estimated: `4326`
|
||||
// Minimum execution time: 106_450_000 picoseconds.
|
||||
Weight::from_parts(111_649_000, 4326)
|
||||
.saturating_add(T::DbWeight::get().reads(9_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(9_u64))
|
||||
}
|
||||
/// Storage: `Tiki::CitizenNft` (r:1 w:0)
|
||||
/// Proof: `Tiki::CitizenNft` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Tiki::UserTikis` (r:1 w:1)
|
||||
/// Proof: `Tiki::UserTikis` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Nfts::Collection` (r:1 w:1)
|
||||
/// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Nfts::ItemConfigOf` (r:1 w:0)
|
||||
/// Proof: `Nfts::ItemConfigOf` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Nfts::CollectionConfigOf` (r:1 w:0)
|
||||
/// Proof: `Nfts::CollectionConfigOf` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Nfts::ItemMetadataOf` (r:1 w:1)
|
||||
/// Proof: `Nfts::ItemMetadataOf` (`max_values`: None, `max_size`: Some(347), added: 2822, mode: `MaxEncodedLen`)
|
||||
fn grant_earned_role() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `848`
|
||||
// Estimated: `3812`
|
||||
// Minimum execution time: 51_486_000 picoseconds.
|
||||
Weight::from_parts(53_580_000, 3812)
|
||||
.saturating_add(T::DbWeight::get().reads(6_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(3_u64))
|
||||
}
|
||||
/// Storage: `Tiki::CitizenNft` (r:1 w:0)
|
||||
/// Proof: `Tiki::CitizenNft` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Tiki::UserTikis` (r:1 w:1)
|
||||
/// Proof: `Tiki::UserTikis` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Nfts::Collection` (r:1 w:1)
|
||||
/// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Nfts::ItemConfigOf` (r:1 w:0)
|
||||
/// Proof: `Nfts::ItemConfigOf` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Nfts::CollectionConfigOf` (r:1 w:0)
|
||||
/// Proof: `Nfts::CollectionConfigOf` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Nfts::ItemMetadataOf` (r:1 w:1)
|
||||
/// Proof: `Nfts::ItemMetadataOf` (`max_values`: None, `max_size`: Some(347), added: 2822, mode: `MaxEncodedLen`)
|
||||
fn grant_elected_role() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `848`
|
||||
// Estimated: `3812`
|
||||
// Minimum execution time: 52_085_000 picoseconds.
|
||||
Weight::from_parts(53_750_000, 3812)
|
||||
.saturating_add(T::DbWeight::get().reads(6_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(3_u64))
|
||||
}
|
||||
}
|
||||
|
||||
// For backwards compatibility and tests.
|
||||
impl WeightInfo for () {
|
||||
/// Storage: `Tiki::CitizenNft` (r:1 w:0)
|
||||
/// Proof: `Tiki::CitizenNft` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Tiki::UserTikis` (r:1 w:1)
|
||||
/// Proof: `Tiki::UserTikis` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Nfts::Collection` (r:1 w:1)
|
||||
/// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Nfts::ItemConfigOf` (r:1 w:0)
|
||||
/// Proof: `Nfts::ItemConfigOf` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Nfts::CollectionConfigOf` (r:1 w:0)
|
||||
/// Proof: `Nfts::CollectionConfigOf` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Nfts::ItemMetadataOf` (r:1 w:1)
|
||||
/// Proof: `Nfts::ItemMetadataOf` (`max_values`: None, `max_size`: Some(347), added: 2822, mode: `MaxEncodedLen`)
|
||||
fn grant_tiki() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `848`
|
||||
// Estimated: `3812`
|
||||
// Minimum execution time: 50_144_000 picoseconds.
|
||||
Weight::from_parts(51_105_000, 3812)
|
||||
.saturating_add(RocksDbWeight::get().reads(6_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(3_u64))
|
||||
}
|
||||
/// Storage: `Tiki::UserTikis` (r:1 w:1)
|
||||
/// Proof: `Tiki::UserTikis` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Tiki::CitizenNft` (r:1 w:0)
|
||||
/// Proof: `Tiki::CitizenNft` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Nfts::Collection` (r:1 w:1)
|
||||
/// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Nfts::ItemConfigOf` (r:1 w:0)
|
||||
/// Proof: `Nfts::ItemConfigOf` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Nfts::CollectionConfigOf` (r:1 w:0)
|
||||
/// Proof: `Nfts::CollectionConfigOf` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Nfts::ItemMetadataOf` (r:1 w:1)
|
||||
/// Proof: `Nfts::ItemMetadataOf` (`max_values`: None, `max_size`: Some(347), added: 2822, mode: `MaxEncodedLen`)
|
||||
fn revoke_tiki() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `850`
|
||||
// Estimated: `3812`
|
||||
// Minimum execution time: 48_006_000 picoseconds.
|
||||
Weight::from_parts(49_738_000, 3812)
|
||||
.saturating_add(RocksDbWeight::get().reads(6_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(3_u64))
|
||||
}
|
||||
/// Storage: `Tiki::CitizenNft` (r:1 w:1)
|
||||
/// Proof: `Tiki::CitizenNft` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Tiki::NextItemId` (r:1 w:1)
|
||||
/// Proof: `Tiki::NextItemId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Nfts::Item` (r:1 w:1)
|
||||
/// Proof: `Nfts::Item` (`max_values`: None, `max_size`: Some(861), added: 3336, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Nfts::Collection` (r:1 w:1)
|
||||
/// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Nfts::CollectionConfigOf` (r:1 w:0)
|
||||
/// Proof: `Nfts::CollectionConfigOf` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Nfts::ItemConfigOf` (r:1 w:1)
|
||||
/// Proof: `Nfts::ItemConfigOf` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Nfts::Attribute` (r:1 w:1)
|
||||
/// Proof: `Nfts::Attribute` (`max_values`: None, `max_size`: Some(479), added: 2954, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Tiki::UserTikis` (r:1 w:1)
|
||||
/// Proof: `Tiki::UserTikis` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Nfts::ItemMetadataOf` (r:1 w:1)
|
||||
/// Proof: `Nfts::ItemMetadataOf` (`max_values`: None, `max_size`: Some(347), added: 2822, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Nfts::Account` (r:0 w:1)
|
||||
/// Proof: `Nfts::Account` (`max_values`: None, `max_size`: Some(88), added: 2563, mode: `MaxEncodedLen`)
|
||||
fn force_mint_citizen_nft() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `439`
|
||||
// Estimated: `4326`
|
||||
// Minimum execution time: 106_450_000 picoseconds.
|
||||
Weight::from_parts(111_649_000, 4326)
|
||||
.saturating_add(RocksDbWeight::get().reads(9_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(9_u64))
|
||||
}
|
||||
/// Storage: `Tiki::CitizenNft` (r:1 w:0)
|
||||
/// Proof: `Tiki::CitizenNft` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Tiki::UserTikis` (r:1 w:1)
|
||||
/// Proof: `Tiki::UserTikis` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Nfts::Collection` (r:1 w:1)
|
||||
/// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Nfts::ItemConfigOf` (r:1 w:0)
|
||||
/// Proof: `Nfts::ItemConfigOf` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Nfts::CollectionConfigOf` (r:1 w:0)
|
||||
/// Proof: `Nfts::CollectionConfigOf` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Nfts::ItemMetadataOf` (r:1 w:1)
|
||||
/// Proof: `Nfts::ItemMetadataOf` (`max_values`: None, `max_size`: Some(347), added: 2822, mode: `MaxEncodedLen`)
|
||||
fn grant_earned_role() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `848`
|
||||
// Estimated: `3812`
|
||||
// Minimum execution time: 51_486_000 picoseconds.
|
||||
Weight::from_parts(53_580_000, 3812)
|
||||
.saturating_add(RocksDbWeight::get().reads(6_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(3_u64))
|
||||
}
|
||||
/// Storage: `Tiki::CitizenNft` (r:1 w:0)
|
||||
/// Proof: `Tiki::CitizenNft` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Tiki::UserTikis` (r:1 w:1)
|
||||
/// Proof: `Tiki::UserTikis` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Nfts::Collection` (r:1 w:1)
|
||||
/// Proof: `Nfts::Collection` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Nfts::ItemConfigOf` (r:1 w:0)
|
||||
/// Proof: `Nfts::ItemConfigOf` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Nfts::CollectionConfigOf` (r:1 w:0)
|
||||
/// Proof: `Nfts::CollectionConfigOf` (`max_values`: None, `max_size`: Some(73), added: 2548, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Nfts::ItemMetadataOf` (r:1 w:1)
|
||||
/// Proof: `Nfts::ItemMetadataOf` (`max_values`: None, `max_size`: Some(347), added: 2822, mode: `MaxEncodedLen`)
|
||||
fn grant_elected_role() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `848`
|
||||
// Estimated: `3812`
|
||||
// Minimum execution time: 52_085_000 picoseconds.
|
||||
Weight::from_parts(53_750_000, 3812)
|
||||
.saturating_add(RocksDbWeight::get().reads(6_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(3_u64))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
[package]
|
||||
name = "pezpallet-token-wrapper"
|
||||
version = "1.0.0"
|
||||
description = "Token Wrapper Pallet for wrapping native HEZ into wHEZ asset"
|
||||
authors.workspace = true
|
||||
homepage.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
publish = false
|
||||
repository.workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
||||
[dependencies]
|
||||
codec = { package = "parity-scale-codec", version = "3.6.12", default-features = false, features = [
|
||||
"derive",
|
||||
"max-encoded-len",
|
||||
] }
|
||||
scale-info = { default-features = false, features = [
|
||||
"derive",
|
||||
], workspace = true }
|
||||
serde = { version = "1.0", default-features = false, features = [
|
||||
"derive",
|
||||
], optional = true }
|
||||
|
||||
pezframe-benchmarking = { optional = true, workspace = true }
|
||||
pezframe-support = { default-features = false, workspace = true }
|
||||
pezframe-system = { default-features = false, workspace = true }
|
||||
pezsp-core = { workspace = true, default-features = false, optional = true }
|
||||
pezsp-io = { workspace = true, default-features = false, optional = true }
|
||||
pezsp-runtime = { default-features = false, workspace = true }
|
||||
pezsp-std = { default-features = false, workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
pezpallet-assets = { workspace = true }
|
||||
pezpallet-balances = { workspace = true }
|
||||
serde = { version = "1.0" }
|
||||
pezsp-core = { workspace = true }
|
||||
pezsp-io = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = [
|
||||
"codec/std",
|
||||
"pezframe-benchmarking?/std",
|
||||
"pezframe-support/std",
|
||||
"pezframe-system/std",
|
||||
"pezpallet-assets/std",
|
||||
"pezpallet-balances/std",
|
||||
"scale-info/std",
|
||||
"serde",
|
||||
"pezsp-core?/std",
|
||||
"pezsp-io?/std",
|
||||
"pezsp-runtime/std",
|
||||
"pezsp-std/std",
|
||||
]
|
||||
runtime-benchmarks = [
|
||||
"pezframe-benchmarking/runtime-benchmarks",
|
||||
"pezframe-support/runtime-benchmarks",
|
||||
"pezframe-system/runtime-benchmarks",
|
||||
"pezpallet-assets/runtime-benchmarks",
|
||||
"pezpallet-balances/runtime-benchmarks",
|
||||
"pezsp-core",
|
||||
"pezsp-io",
|
||||
"pezsp-io?/runtime-benchmarks",
|
||||
"pezsp-runtime/runtime-benchmarks",
|
||||
]
|
||||
try-runtime = [
|
||||
"pezframe-support/try-runtime",
|
||||
"pezframe-system/try-runtime",
|
||||
"pezpallet-assets/try-runtime",
|
||||
"pezpallet-balances/try-runtime",
|
||||
"pezsp-runtime/try-runtime",
|
||||
]
|
||||
@@ -0,0 +1,68 @@
|
||||
//! Benchmarking setup for pezpallet-token-wrapper
|
||||
|
||||
#![cfg(feature = "runtime-benchmarks")]
|
||||
|
||||
use super::*;
|
||||
#[allow(unused)]
|
||||
use crate::Pallet as TokenWrapper;
|
||||
use pezframe_benchmarking::v2::*;
|
||||
use pezframe_support::traits::Currency;
|
||||
use pezframe_system::RawOrigin;
|
||||
|
||||
#[benchmarks]
|
||||
mod benchmarks {
|
||||
use super::*;
|
||||
|
||||
#[benchmark]
|
||||
fn wrap() {
|
||||
let caller: T::AccountId = whitelisted_caller();
|
||||
let pezpallet_account = Pallet::<T>::account_id();
|
||||
let amount = 10_000u32.into();
|
||||
|
||||
// Fund both caller and pallet account
|
||||
let funding = <T::Currency as Currency<T::AccountId>>::minimum_balance()
|
||||
.saturating_mul(1000u32.into());
|
||||
|
||||
T::Currency::make_free_balance_be(&caller, funding);
|
||||
T::Currency::make_free_balance_be(&pezpallet_account, funding);
|
||||
|
||||
// Create asset
|
||||
let _ =
|
||||
T::Assets::create(T::WrapperAssetId::get(), pezpallet_account.clone(), true, 1u32.into());
|
||||
|
||||
#[extrinsic_call]
|
||||
_(RawOrigin::Signed(caller.clone()), amount);
|
||||
|
||||
// Verify
|
||||
assert!(T::Assets::balance(T::WrapperAssetId::get(), &caller) >= amount);
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn unwrap() {
|
||||
let caller: T::AccountId = whitelisted_caller();
|
||||
let pezpallet_account = Pallet::<T>::account_id();
|
||||
let amount = 10_000u32.into();
|
||||
|
||||
// Fund both accounts
|
||||
let funding = <T::Currency as Currency<T::AccountId>>::minimum_balance()
|
||||
.saturating_mul(1000u32.into());
|
||||
|
||||
T::Currency::make_free_balance_be(&caller, funding);
|
||||
T::Currency::make_free_balance_be(&pezpallet_account, funding);
|
||||
|
||||
// Create asset
|
||||
let _ =
|
||||
T::Assets::create(T::WrapperAssetId::get(), pezpallet_account.clone(), true, 1u32.into());
|
||||
|
||||
// Wrap first
|
||||
let _ = Pallet::<T>::wrap(RawOrigin::Signed(caller.clone()).into(), amount);
|
||||
|
||||
#[extrinsic_call]
|
||||
_(RawOrigin::Signed(caller.clone()), amount);
|
||||
|
||||
// Verify
|
||||
assert_eq!(T::Assets::balance(T::WrapperAssetId::get(), &caller), 0u32.into());
|
||||
}
|
||||
|
||||
impl_benchmark_test_suite!(Pallet, crate::mock::new_test_ext(), crate::mock::Test);
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
//! # Token Wrapper Pallet
|
||||
//!
|
||||
//! A pallet for wrapping native tokens (HEZ) into fungible assets (wHEZ)
|
||||
//! to enable DEX operations between native and asset tokens.
|
||||
//!
|
||||
//! ## Overview
|
||||
//!
|
||||
//! This pallet provides:
|
||||
//! - `wrap`: Convert native HEZ to wHEZ (Asset ID 0)
|
||||
//! - `unwrap`: Convert wHEZ back to native HEZ
|
||||
//!
|
||||
//! The pallet maintains a 1:1 backing between HEZ and wHEZ.
|
||||
|
||||
pub use pallet::*;
|
||||
pub use weights::WeightInfo;
|
||||
pub mod weights;
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
mod benchmarking;
|
||||
#[cfg(test)]
|
||||
mod mock;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
use pezframe_support::{
|
||||
dispatch::DispatchResult,
|
||||
pezpallet_prelude::*,
|
||||
traits::{
|
||||
fungibles::{Create, Inspect, Mutate},
|
||||
Currency, ExistenceRequirement,
|
||||
},
|
||||
PalletId,
|
||||
};
|
||||
use pezframe_system::pezpallet_prelude::*;
|
||||
use pezsp_runtime::traits::{AccountIdConversion, Saturating, Zero};
|
||||
|
||||
#[pezframe_support::pallet]
|
||||
pub mod pallet {
|
||||
use super::*;
|
||||
|
||||
type BalanceOf<T> =
|
||||
<<T as Config>::Currency as Currency<<T as pezframe_system::Config>::AccountId>>::Balance;
|
||||
|
||||
#[pallet::pallet]
|
||||
pub struct Pallet<T>(_);
|
||||
|
||||
#[pallet::config]
|
||||
pub trait Config: pezframe_system::Config {
|
||||
/// Weight information for extrinsics in this pallet.
|
||||
type WeightInfo: crate::WeightInfo;
|
||||
|
||||
/// Native currency (HEZ)
|
||||
type Currency: Currency<Self::AccountId>;
|
||||
|
||||
/// Asset ID type
|
||||
type AssetId: Parameter + Member + Copy + MaybeSerializeDeserialize + MaxEncodedLen;
|
||||
|
||||
/// Fungible assets (for wHEZ)
|
||||
type Assets: Inspect<Self::AccountId, AssetId = Self::AssetId, Balance = BalanceOf<Self>>
|
||||
+ Mutate<Self::AccountId>
|
||||
+ Create<Self::AccountId>;
|
||||
|
||||
/// Pallet ID for the wrapper account
|
||||
#[pallet::constant]
|
||||
type PalletId: Get<PalletId>;
|
||||
|
||||
/// Asset ID for wrapped token (wHEZ)
|
||||
#[pallet::constant]
|
||||
type WrapperAssetId: Get<Self::AssetId>;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// STORAGE ITEMS
|
||||
// ============================================================================
|
||||
|
||||
/// Total amount of native tokens locked in wrapper
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn total_locked)]
|
||||
pub type TotalLocked<T: Config> = StorageValue<_, BalanceOf<T>, ValueQuery>;
|
||||
|
||||
// ============================================================================
|
||||
// EVENTS
|
||||
// ============================================================================
|
||||
|
||||
#[pallet::event]
|
||||
#[pallet::generate_deposit(pub(super) fn deposit_event)]
|
||||
pub enum Event<T: Config> {
|
||||
/// Native token wrapped into asset token. [who, amount]
|
||||
Wrapped { who: T::AccountId, amount: BalanceOf<T> },
|
||||
/// Asset token unwrapped back to native. [who, amount]
|
||||
Unwrapped { who: T::AccountId, amount: BalanceOf<T> },
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ERRORS
|
||||
// ============================================================================
|
||||
|
||||
#[pallet::error]
|
||||
pub enum Error<T> {
|
||||
/// Insufficient balance for wrapping
|
||||
InsufficientBalance,
|
||||
/// Insufficient wrapped tokens for unwrapping
|
||||
InsufficientWrappedBalance,
|
||||
/// Transfer failed
|
||||
TransferFailed,
|
||||
/// Mint failed
|
||||
MintFailed,
|
||||
/// Burn failed
|
||||
BurnFailed,
|
||||
/// Amount is zero
|
||||
ZeroAmount,
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// DISPATCHABLE FUNCTIONS
|
||||
// ============================================================================
|
||||
|
||||
#[pallet::call]
|
||||
impl<T: Config> Pallet<T> {
|
||||
/// Wrap native tokens (HEZ) into wrapped asset tokens (wHEZ)
|
||||
///
|
||||
/// - `amount`: The amount of native tokens to wrap
|
||||
///
|
||||
/// This will:
|
||||
/// 1. Transfer native tokens from user to pallet account (lock)
|
||||
/// 2. Mint equivalent amount of wrapped tokens to user
|
||||
///
|
||||
/// Emits `Wrapped` event.
|
||||
#[pallet::call_index(0)]
|
||||
#[pallet::weight(T::WeightInfo::wrap())]
|
||||
pub fn wrap(
|
||||
origin: OriginFor<T>,
|
||||
#[pallet::compact] amount: BalanceOf<T>,
|
||||
) -> DispatchResult {
|
||||
let who = ensure_signed(origin)?;
|
||||
|
||||
// Ensure amount is not zero
|
||||
ensure!(!amount.is_zero(), Error::<T>::ZeroAmount);
|
||||
|
||||
// Check balance
|
||||
ensure!(T::Currency::free_balance(&who) >= amount, Error::<T>::InsufficientBalance);
|
||||
|
||||
// Transfer native tokens to pallet account (lock them)
|
||||
T::Currency::transfer(
|
||||
&who,
|
||||
&Self::account_id(),
|
||||
amount,
|
||||
ExistenceRequirement::KeepAlive,
|
||||
)
|
||||
.map_err(|_| Error::<T>::TransferFailed)?;
|
||||
|
||||
// Update total locked
|
||||
TotalLocked::<T>::mutate(|total| {
|
||||
*total = total.saturating_add(amount);
|
||||
});
|
||||
|
||||
// Mint wrapped tokens to user
|
||||
T::Assets::mint_into(T::WrapperAssetId::get(), &who, amount)
|
||||
.map_err(|_| Error::<T>::MintFailed)?;
|
||||
|
||||
Self::deposit_event(Event::Wrapped { who, amount });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Unwrap wrapped asset tokens (wHEZ) back to native tokens (HEZ)
|
||||
///
|
||||
/// - `amount`: The amount of wrapped tokens to unwrap
|
||||
///
|
||||
/// This will:
|
||||
/// 1. Burn wrapped tokens from user
|
||||
/// 2. Transfer equivalent native tokens back to user (unlock)
|
||||
///
|
||||
/// Emits `Unwrapped` event.
|
||||
#[pallet::call_index(1)]
|
||||
#[pallet::weight(T::WeightInfo::unwrap())]
|
||||
pub fn unwrap(
|
||||
origin: OriginFor<T>,
|
||||
#[pallet::compact] amount: BalanceOf<T>,
|
||||
) -> DispatchResult {
|
||||
let who = ensure_signed(origin)?;
|
||||
|
||||
// Ensure amount is not zero
|
||||
ensure!(!amount.is_zero(), Error::<T>::ZeroAmount);
|
||||
|
||||
// Check wrapped token balance
|
||||
let wrapped_balance = T::Assets::balance(T::WrapperAssetId::get(), &who);
|
||||
ensure!(wrapped_balance >= amount, Error::<T>::InsufficientWrappedBalance);
|
||||
|
||||
// Burn wrapped tokens from user
|
||||
T::Assets::burn_from(
|
||||
T::WrapperAssetId::get(),
|
||||
&who,
|
||||
amount,
|
||||
pezframe_support::traits::tokens::Preservation::Expendable,
|
||||
pezframe_support::traits::tokens::Precision::Exact,
|
||||
pezframe_support::traits::tokens::Fortitude::Force,
|
||||
)
|
||||
.map_err(|_| Error::<T>::BurnFailed)?;
|
||||
|
||||
// Update total locked
|
||||
TotalLocked::<T>::mutate(|total| {
|
||||
*total = total.saturating_sub(amount);
|
||||
});
|
||||
|
||||
// Transfer native tokens back to user (unlock)
|
||||
T::Currency::transfer(
|
||||
&Self::account_id(),
|
||||
&who,
|
||||
amount,
|
||||
ExistenceRequirement::AllowDeath,
|
||||
)
|
||||
.map_err(|_| Error::<T>::TransferFailed)?;
|
||||
|
||||
Self::deposit_event(Event::Unwrapped { who, amount });
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// ============================================================================
|
||||
|
||||
impl<T: Config> Pallet<T> {
|
||||
/// Get the account ID of the pallet
|
||||
pub fn account_id() -> T::AccountId {
|
||||
T::PalletId::get().into_account_truncating()
|
||||
}
|
||||
|
||||
/// Get the total supply of wrapped tokens
|
||||
pub fn total_wrapped() -> BalanceOf<T> {
|
||||
T::Assets::total_issuance(T::WrapperAssetId::get())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
use crate as pezpallet_token_wrapper;
|
||||
use pezframe_support::{
|
||||
construct_runtime, parameter_types,
|
||||
traits::{AsEnsureOriginWithArg, ConstU128, ConstU32, Everything},
|
||||
PalletId,
|
||||
};
|
||||
use pezframe_system as system;
|
||||
use pezsp_core::H256;
|
||||
use pezsp_runtime::{
|
||||
traits::{BlakeTwo256, IdentityLookup},
|
||||
BuildStorage,
|
||||
};
|
||||
|
||||
pub type AccountId = u64;
|
||||
pub type Balance = u128;
|
||||
pub type AssetId = u32;
|
||||
|
||||
// Configure a mock runtime to test the pallet.
|
||||
construct_runtime!(
|
||||
pub enum Test {
|
||||
System: pezframe_system,
|
||||
Balances: pezpallet_balances,
|
||||
Assets: pezpallet_assets,
|
||||
TokenWrapper: pezpallet_token_wrapper,
|
||||
}
|
||||
);
|
||||
|
||||
parameter_types! {
|
||||
pub const BlockHashCount: u64 = 250;
|
||||
pub const SS58Prefix: u8 = 42;
|
||||
}
|
||||
|
||||
impl system::Config for Test {
|
||||
type BaseCallFilter = Everything;
|
||||
type BlockWeights = ();
|
||||
type BlockLength = ();
|
||||
type DbWeight = ();
|
||||
type RuntimeOrigin = RuntimeOrigin;
|
||||
type RuntimeCall = RuntimeCall;
|
||||
type RuntimeTask = ();
|
||||
type Nonce = u64;
|
||||
type Hash = H256;
|
||||
type Hashing = BlakeTwo256;
|
||||
type AccountId = AccountId;
|
||||
type Lookup = IdentityLookup<Self::AccountId>;
|
||||
type Block = pezframe_system::mocking::MockBlock<Test>;
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type BlockHashCount = BlockHashCount;
|
||||
type Version = ();
|
||||
type PalletInfo = PalletInfo;
|
||||
type AccountData = pezpallet_balances::AccountData<Balance>;
|
||||
type OnNewAccount = ();
|
||||
type OnKilledAccount = ();
|
||||
type SystemWeightInfo = ();
|
||||
type SS58Prefix = SS58Prefix;
|
||||
type OnSetCode = ();
|
||||
type MaxConsumers = ConstU32<16>;
|
||||
type ExtensionsWeightInfo = ();
|
||||
type SingleBlockMigrations = ();
|
||||
type MultiBlockMigrator = ();
|
||||
type PreInherents = ();
|
||||
type PostInherents = ();
|
||||
type PostTransactions = ();
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const ExistentialDeposit: Balance = 1;
|
||||
}
|
||||
|
||||
impl pezpallet_balances::Config for Test {
|
||||
type MaxLocks = ConstU32<50>;
|
||||
type MaxReserves = ConstU32<50>;
|
||||
type ReserveIdentifier = [u8; 8];
|
||||
type Balance = Balance;
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type DustRemoval = ();
|
||||
type ExistentialDeposit = ExistentialDeposit;
|
||||
type AccountStore = System;
|
||||
type WeightInfo = ();
|
||||
type FreezeIdentifier = ();
|
||||
type MaxFreezes = ();
|
||||
type RuntimeHoldReason = ();
|
||||
type RuntimeFreezeReason = ();
|
||||
type DoneSlashHandler = ();
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const AssetDeposit: Balance = 100;
|
||||
pub const ApprovalDeposit: Balance = 1;
|
||||
pub const StringLimit: u32 = 50;
|
||||
pub const MetadataDepositBase: Balance = 10;
|
||||
pub const MetadataDepositPerByte: Balance = 1;
|
||||
}
|
||||
|
||||
impl pezpallet_assets::Config for Test {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type Balance = Balance;
|
||||
type AssetId = AssetId;
|
||||
type AssetIdParameter = u32;
|
||||
type Currency = Balances;
|
||||
type CreateOrigin = AsEnsureOriginWithArg<pezframe_system::EnsureSigned<AccountId>>;
|
||||
type ForceOrigin = pezframe_system::EnsureRoot<AccountId>;
|
||||
type AssetDeposit = AssetDeposit;
|
||||
type AssetAccountDeposit = ConstU128<1>;
|
||||
type MetadataDepositBase = MetadataDepositBase;
|
||||
type MetadataDepositPerByte = MetadataDepositPerByte;
|
||||
type ApprovalDeposit = ApprovalDeposit;
|
||||
type StringLimit = StringLimit;
|
||||
type Freezer = ();
|
||||
type Extra = ();
|
||||
type CallbackHandle = ();
|
||||
type WeightInfo = ();
|
||||
type RemoveItemsLimit = ConstU32<1000>;
|
||||
type Holder = ();
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const TokenWrapperPalletId: PalletId = PalletId(*b"py/wrper");
|
||||
pub const WrapperAssetId: u32 = 0;
|
||||
}
|
||||
|
||||
impl pezpallet_token_wrapper::Config for Test {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type WeightInfo = crate::weights::BizinikiwiWeight<Test>;
|
||||
type Currency = Balances;
|
||||
type Assets = Assets;
|
||||
type PalletId = TokenWrapperPalletId;
|
||||
type WrapperAssetId = WrapperAssetId;
|
||||
}
|
||||
|
||||
// Build genesis storage according to the mock runtime.
|
||||
pub fn new_test_ext() -> pezsp_io::TestExternalities {
|
||||
use pezframe_support::assert_ok;
|
||||
|
||||
let mut storage = system::GenesisConfig::<Test>::default().build_storage().unwrap();
|
||||
|
||||
pezpallet_balances::GenesisConfig::<Test> {
|
||||
balances: vec![(1, 10000), (2, 5000), (3, 3000)],
|
||||
dev_accounts: None,
|
||||
}
|
||||
.assimilate_storage(&mut storage)
|
||||
.unwrap();
|
||||
|
||||
let mut ext = pezsp_io::TestExternalities::new(storage);
|
||||
ext.execute_with(|| {
|
||||
System::set_block_number(1);
|
||||
|
||||
// Create wHEZ asset (Asset ID 0)
|
||||
assert_ok!(Assets::force_create(
|
||||
RuntimeOrigin::root(),
|
||||
0, // Asset ID
|
||||
TokenWrapper::account_id(), // Owner = pallet account
|
||||
true, // is_sufficient
|
||||
1, // min_balance
|
||||
));
|
||||
});
|
||||
ext
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
use super::*;
|
||||
use crate::mock::*;
|
||||
use pezframe_support::{assert_noop, assert_ok};
|
||||
|
||||
#[test]
|
||||
fn wrap_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let user = 1;
|
||||
let amount = 1000;
|
||||
|
||||
assert_eq!(Balances::free_balance(&user), 10000);
|
||||
assert_eq!(Assets::balance(0, &user), 0);
|
||||
|
||||
assert_ok!(TokenWrapper::wrap(RuntimeOrigin::signed(user), amount));
|
||||
|
||||
assert_eq!(Balances::free_balance(&user), 10000 - amount);
|
||||
assert_eq!(Assets::balance(0, &user), amount);
|
||||
assert_eq!(TokenWrapper::total_locked(), amount);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unwrap_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let user = 1;
|
||||
let amount = 1000;
|
||||
|
||||
assert_ok!(TokenWrapper::wrap(RuntimeOrigin::signed(user), amount));
|
||||
let native_balance = Balances::free_balance(&user);
|
||||
|
||||
assert_ok!(TokenWrapper::unwrap(RuntimeOrigin::signed(user), amount));
|
||||
|
||||
assert_eq!(Balances::free_balance(&user), native_balance + amount);
|
||||
assert_eq!(Assets::balance(0, &user), 0);
|
||||
assert_eq!(TokenWrapper::total_locked(), 0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrap_fails_insufficient_balance() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let user = 1;
|
||||
let amount = 20000;
|
||||
|
||||
assert_noop!(
|
||||
TokenWrapper::wrap(RuntimeOrigin::signed(user), amount),
|
||||
Error::<Test>::InsufficientBalance
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unwrap_fails_insufficient_wrapped_balance() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let user = 1;
|
||||
let amount = 1000;
|
||||
|
||||
assert_noop!(
|
||||
TokenWrapper::unwrap(RuntimeOrigin::signed(user), amount),
|
||||
Error::<Test>::InsufficientWrappedBalance
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// EDGE CASE TESTS
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn wrap_fails_zero_amount() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let user = 1;
|
||||
|
||||
assert_noop!(TokenWrapper::wrap(RuntimeOrigin::signed(user), 0), Error::<Test>::ZeroAmount);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unwrap_fails_zero_amount() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let user = 1;
|
||||
let amount = 1000;
|
||||
|
||||
// First wrap some tokens
|
||||
assert_ok!(TokenWrapper::wrap(RuntimeOrigin::signed(user), amount));
|
||||
|
||||
// Try to unwrap zero
|
||||
assert_noop!(
|
||||
TokenWrapper::unwrap(RuntimeOrigin::signed(user), 0),
|
||||
Error::<Test>::ZeroAmount
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_user_concurrent_wrap_unwrap() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let user1 = 1;
|
||||
let user2 = 2;
|
||||
let user3 = 3;
|
||||
|
||||
let amount1 = 1000;
|
||||
let amount2 = 2000;
|
||||
let amount3 = 1500;
|
||||
|
||||
// All users wrap
|
||||
assert_ok!(TokenWrapper::wrap(RuntimeOrigin::signed(user1), amount1));
|
||||
assert_ok!(TokenWrapper::wrap(RuntimeOrigin::signed(user2), amount2));
|
||||
assert_ok!(TokenWrapper::wrap(RuntimeOrigin::signed(user3), amount3));
|
||||
|
||||
// Verify balances
|
||||
assert_eq!(Assets::balance(0, &user1), amount1);
|
||||
assert_eq!(Assets::balance(0, &user2), amount2);
|
||||
assert_eq!(Assets::balance(0, &user3), amount3);
|
||||
|
||||
// Verify total locked
|
||||
assert_eq!(TokenWrapper::total_locked(), amount1 + amount2 + amount3);
|
||||
|
||||
// User 2 unwraps
|
||||
assert_ok!(TokenWrapper::unwrap(RuntimeOrigin::signed(user2), amount2));
|
||||
assert_eq!(Assets::balance(0, &user2), 0);
|
||||
assert_eq!(TokenWrapper::total_locked(), amount1 + amount3);
|
||||
|
||||
// User 1 and 3 still have their wrapped tokens
|
||||
assert_eq!(Assets::balance(0, &user1), amount1);
|
||||
assert_eq!(Assets::balance(0, &user3), amount3);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_wrap_operations_same_user() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let user = 1;
|
||||
|
||||
// Multiple wraps
|
||||
assert_ok!(TokenWrapper::wrap(RuntimeOrigin::signed(user), 100));
|
||||
assert_ok!(TokenWrapper::wrap(RuntimeOrigin::signed(user), 200));
|
||||
assert_ok!(TokenWrapper::wrap(RuntimeOrigin::signed(user), 300));
|
||||
|
||||
// Verify accumulated balance
|
||||
assert_eq!(Assets::balance(0, &user), 600);
|
||||
assert_eq!(TokenWrapper::total_locked(), 600);
|
||||
|
||||
// Partial unwrap
|
||||
assert_ok!(TokenWrapper::unwrap(RuntimeOrigin::signed(user), 250));
|
||||
assert_eq!(Assets::balance(0, &user), 350);
|
||||
assert_eq!(TokenWrapper::total_locked(), 350);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn events_emitted_correctly() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let user = 1;
|
||||
let amount = 1000;
|
||||
|
||||
// Wrap and check event
|
||||
assert_ok!(TokenWrapper::wrap(RuntimeOrigin::signed(user), amount));
|
||||
System::assert_has_event(Event::Wrapped { who: user, amount }.into());
|
||||
|
||||
// Unwrap and check event
|
||||
assert_ok!(TokenWrapper::unwrap(RuntimeOrigin::signed(user), amount));
|
||||
System::assert_has_event(Event::Unwrapped { who: user, amount }.into());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn total_locked_tracking_accuracy() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_eq!(TokenWrapper::total_locked(), 0);
|
||||
|
||||
let user1 = 1;
|
||||
let user2 = 2;
|
||||
|
||||
// User 1 wraps
|
||||
assert_ok!(TokenWrapper::wrap(RuntimeOrigin::signed(user1), 1000));
|
||||
assert_eq!(TokenWrapper::total_locked(), 1000);
|
||||
|
||||
// User 2 wraps
|
||||
assert_ok!(TokenWrapper::wrap(RuntimeOrigin::signed(user2), 500));
|
||||
assert_eq!(TokenWrapper::total_locked(), 1500);
|
||||
|
||||
// User 1 unwraps partially
|
||||
assert_ok!(TokenWrapper::unwrap(RuntimeOrigin::signed(user1), 300));
|
||||
assert_eq!(TokenWrapper::total_locked(), 1200);
|
||||
|
||||
// User 2 unwraps all
|
||||
assert_ok!(TokenWrapper::unwrap(RuntimeOrigin::signed(user2), 500));
|
||||
assert_eq!(TokenWrapper::total_locked(), 700);
|
||||
|
||||
// User 1 unwraps remaining
|
||||
assert_ok!(TokenWrapper::unwrap(RuntimeOrigin::signed(user1), 700));
|
||||
assert_eq!(TokenWrapper::total_locked(), 0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn large_amount_wrap_unwrap() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let user = 1;
|
||||
// User has 10000 initial balance
|
||||
let large_amount = 9000; // Leave some for existential deposit
|
||||
|
||||
assert_ok!(TokenWrapper::wrap(RuntimeOrigin::signed(user), large_amount));
|
||||
assert_eq!(Assets::balance(0, &user), large_amount);
|
||||
assert_eq!(TokenWrapper::total_locked(), large_amount);
|
||||
|
||||
assert_ok!(TokenWrapper::unwrap(RuntimeOrigin::signed(user), large_amount));
|
||||
assert_eq!(Assets::balance(0, &user), 0);
|
||||
assert_eq!(TokenWrapper::total_locked(), 0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pezpallet_account_balance_consistency() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let user = 1;
|
||||
let amount = 1000;
|
||||
let pezpallet_account = TokenWrapper::account_id();
|
||||
|
||||
let initial_pallet_balance = Balances::free_balance(&pezpallet_account);
|
||||
|
||||
// Wrap - pallet account should receive native tokens
|
||||
assert_ok!(TokenWrapper::wrap(RuntimeOrigin::signed(user), amount));
|
||||
assert_eq!(Balances::free_balance(&pezpallet_account), initial_pallet_balance + amount);
|
||||
|
||||
// Unwrap - pallet account should release native tokens
|
||||
assert_ok!(TokenWrapper::unwrap(RuntimeOrigin::signed(user), amount));
|
||||
assert_eq!(Balances::free_balance(&pezpallet_account), initial_pallet_balance);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrap_unwrap_maintains_1_to_1_backing() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let users = vec![1, 2, 3];
|
||||
let amounts = vec![1000, 2000, 1500];
|
||||
|
||||
// All users wrap
|
||||
for (user, amount) in users.iter().zip(amounts.iter()) {
|
||||
assert_ok!(TokenWrapper::wrap(RuntimeOrigin::signed(*user), *amount));
|
||||
}
|
||||
|
||||
let total_wrapped = amounts.iter().sum::<u128>();
|
||||
let pezpallet_account = TokenWrapper::account_id();
|
||||
let pezpallet_balance = Balances::free_balance(&pezpallet_account);
|
||||
|
||||
// Pallet should hold exactly the amount of wrapped tokens
|
||||
// (Note: may include existential deposit, so check >= total_wrapped)
|
||||
assert!(pezpallet_balance >= total_wrapped);
|
||||
assert_eq!(TokenWrapper::total_locked(), total_wrapped);
|
||||
|
||||
// Verify total supply matches
|
||||
assert_eq!(Assets::total_issuance(0), total_wrapped);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
|
||||
//! Autogenerated weights for `pezpallet_token_wrapper`
|
||||
//!
|
||||
//! THIS FILE WAS AUTO-GENERATED USING THE BIZINIKIWI BENCHMARK CLI VERSION 32.0.0
|
||||
//! DATE: 2025-12-08, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
|
||||
//! WORST CASE MAP SIZE: `1000000`
|
||||
//! HOSTNAME: `MamostePC`, CPU: `11th Gen Intel(R) Core(TM) i9-11950H @ 2.60GHz`
|
||||
//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: `1024`
|
||||
|
||||
// Executed Command:
|
||||
// ./target/release/frame-omni-bencher
|
||||
// v1
|
||||
// benchmark
|
||||
// pallet
|
||||
// --runtime
|
||||
// target/release/wbuild/asset-hub-pezkuwichain-runtime/asset_hub_pezkuwichain_runtime.compact.compressed.wasm
|
||||
// --pallets
|
||||
// pezpallet_token_wrapper
|
||||
// -e
|
||||
// all
|
||||
// --steps
|
||||
// 50
|
||||
// --repeat
|
||||
// 20
|
||||
// --output
|
||||
// pezcumulus/teyrchains/pallets/token-wrapper/src/weights.rs
|
||||
// --template
|
||||
// bizinikiwi/.maintain/frame-weight-template.hbs
|
||||
|
||||
#![cfg_attr(rustfmt, rustfmt_skip)]
|
||||
#![allow(unused_parens)]
|
||||
#![allow(unused_imports)]
|
||||
#![allow(missing_docs)]
|
||||
#![allow(dead_code)]
|
||||
|
||||
use pezframe_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}};
|
||||
use core::marker::PhantomData;
|
||||
|
||||
/// Weight functions needed for `pezpallet_token_wrapper`.
|
||||
pub trait WeightInfo {
|
||||
fn wrap() -> Weight;
|
||||
fn unwrap() -> Weight;
|
||||
}
|
||||
|
||||
/// Weights for `pezpallet_token_wrapper` using the Bizinikiwi node and recommended hardware.
|
||||
pub struct BizinikiwiWeight<T>(PhantomData<T>);
|
||||
impl<T: pezframe_system::Config> WeightInfo for BizinikiwiWeight<T> {
|
||||
/// Storage: `System::Account` (r:1 w:1)
|
||||
/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
|
||||
/// Storage: `TokenWrapper::TotalLocked` (r:1 w:1)
|
||||
/// Proof: `TokenWrapper::TotalLocked` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Assets::Asset` (r:1 w:1)
|
||||
/// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Assets::Account` (r:1 w:1)
|
||||
/// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
|
||||
fn wrap() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `631`
|
||||
// Estimated: `3675`
|
||||
// Minimum execution time: 61_975_000 picoseconds.
|
||||
Weight::from_parts(63_198_000, 3675)
|
||||
.saturating_add(T::DbWeight::get().reads(4_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(4_u64))
|
||||
}
|
||||
/// Storage: `Assets::Account` (r:1 w:1)
|
||||
/// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Assets::Asset` (r:1 w:1)
|
||||
/// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
|
||||
/// Storage: `AssetsFreezer::FrozenBalances` (r:1 w:1)
|
||||
/// Proof: `AssetsFreezer::FrozenBalances` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
|
||||
/// Storage: `AssetsFreezer::Freezes` (r:1 w:1)
|
||||
/// Proof: `AssetsFreezer::Freezes` (`max_values`: None, `max_size`: Some(105), added: 2580, mode: `MaxEncodedLen`)
|
||||
/// Storage: `TokenWrapper::TotalLocked` (r:1 w:1)
|
||||
/// Proof: `TokenWrapper::TotalLocked` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`)
|
||||
/// Storage: `System::Account` (r:1 w:1)
|
||||
/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
|
||||
fn unwrap() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `713`
|
||||
// Estimated: `3675`
|
||||
// Minimum execution time: 87_171_000 picoseconds.
|
||||
Weight::from_parts(89_650_000, 3675)
|
||||
.saturating_add(T::DbWeight::get().reads(6_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(6_u64))
|
||||
}
|
||||
}
|
||||
|
||||
// For backwards compatibility and tests.
|
||||
impl WeightInfo for () {
|
||||
/// Storage: `System::Account` (r:1 w:1)
|
||||
/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
|
||||
/// Storage: `TokenWrapper::TotalLocked` (r:1 w:1)
|
||||
/// Proof: `TokenWrapper::TotalLocked` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Assets::Asset` (r:1 w:1)
|
||||
/// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Assets::Account` (r:1 w:1)
|
||||
/// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
|
||||
fn wrap() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `631`
|
||||
// Estimated: `3675`
|
||||
// Minimum execution time: 61_975_000 picoseconds.
|
||||
Weight::from_parts(63_198_000, 3675)
|
||||
.saturating_add(RocksDbWeight::get().reads(4_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(4_u64))
|
||||
}
|
||||
/// Storage: `Assets::Account` (r:1 w:1)
|
||||
/// Proof: `Assets::Account` (`max_values`: None, `max_size`: Some(134), added: 2609, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Assets::Asset` (r:1 w:1)
|
||||
/// Proof: `Assets::Asset` (`max_values`: None, `max_size`: Some(210), added: 2685, mode: `MaxEncodedLen`)
|
||||
/// Storage: `AssetsFreezer::FrozenBalances` (r:1 w:1)
|
||||
/// Proof: `AssetsFreezer::FrozenBalances` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`)
|
||||
/// Storage: `AssetsFreezer::Freezes` (r:1 w:1)
|
||||
/// Proof: `AssetsFreezer::Freezes` (`max_values`: None, `max_size`: Some(105), added: 2580, mode: `MaxEncodedLen`)
|
||||
/// Storage: `TokenWrapper::TotalLocked` (r:1 w:1)
|
||||
/// Proof: `TokenWrapper::TotalLocked` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`)
|
||||
/// Storage: `System::Account` (r:1 w:1)
|
||||
/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
|
||||
fn unwrap() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `713`
|
||||
// Estimated: `3675`
|
||||
// Minimum execution time: 87_171_000 picoseconds.
|
||||
Weight::from_parts(89_650_000, 3675)
|
||||
.saturating_add(RocksDbWeight::get().reads(6_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(6_u64))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
[package]
|
||||
name = "pezpallet-trust"
|
||||
version = "1.0.0"
|
||||
description = "PezkuwiChain Trust Score Engine Pallet"
|
||||
authors.workspace = true
|
||||
homepage.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
publish = false
|
||||
repository.workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
||||
[dependencies]
|
||||
codec = { workspace = true, default-features = false, features = ["derive"] }
|
||||
scale-info = { default-features = false, features = [
|
||||
"derive",
|
||||
], workspace = true }
|
||||
serde = { version = "1.0", default-features = false, features = [
|
||||
"derive",
|
||||
], optional = true }
|
||||
|
||||
pezframe-support = { default-features = false, workspace = true }
|
||||
pezframe-system = { default-features = false, workspace = true }
|
||||
log = { default-features = false, workspace = true }
|
||||
pezsp-runtime = { default-features = false, workspace = true }
|
||||
pezsp-std = { default-features = false, workspace = true }
|
||||
|
||||
# PezkuwiChain'in özel tiplerini ve trait'lerini içeren kütüphane
|
||||
pezkuwi-primitives = { workspace = true, default-features = false }
|
||||
|
||||
# Palet bağımlılıkları (Trust puanı hesaplaması için gerekli)
|
||||
# Standart Bizinikiwi paletleri için path belirtilmez, workspace=true kullanılır.
|
||||
pezpallet-balances = { default-features = false, workspace = true }
|
||||
# Özel PezkuwiChain paletleri için workspace inheritance kullanılır.
|
||||
pezpallet-identity-kyc = { workspace = true, default-features = false }
|
||||
pezpallet-perwerde = { workspace = true, default-features = false }
|
||||
pezpallet-referral = { workspace = true, default-features = false }
|
||||
pezpallet-staking-score = { workspace = true, default-features = false }
|
||||
pezpallet-tiki = { workspace = true, default-features = false }
|
||||
|
||||
|
||||
# --- Test ve Benchmark için Gerekli İsteğe Bağlı Bağımlılıklar ---
|
||||
pezframe-benchmarking = { optional = true, workspace = true }
|
||||
pezsp-core = { workspace = true, default-features = false, optional = true }
|
||||
pezsp-io = { workspace = true, default-features = false, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
# Test için gerekli olan bağımlılıklar
|
||||
pezsp-core = { workspace = true, default-features = false }
|
||||
pezsp-io = { workspace = true, default-features = false }
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = [
|
||||
"codec/std",
|
||||
"pezframe-benchmarking?/std",
|
||||
"pezframe-support/std",
|
||||
"pezframe-system/std",
|
||||
"log/std",
|
||||
# Diğer paletlerin std özellikleri
|
||||
"pezpallet-balances/std",
|
||||
"pezpallet-identity-kyc/std",
|
||||
"pezpallet-perwerde/std",
|
||||
"pezpallet-referral/std",
|
||||
"pezpallet-staking-score/std",
|
||||
"pezpallet-tiki/std",
|
||||
"pezkuwi-primitives/std",
|
||||
"scale-info/std",
|
||||
"serde", # serde'yi doğrudan feature olarak ekliyoruz
|
||||
"serde?/std",
|
||||
"pezsp-core?/std",
|
||||
"pezsp-io?/std",
|
||||
"pezsp-runtime/std",
|
||||
"pezsp-std/std",
|
||||
]
|
||||
runtime-benchmarks = [
|
||||
"pezframe-benchmarking/runtime-benchmarks",
|
||||
"pezframe-support/runtime-benchmarks",
|
||||
"pezframe-system/runtime-benchmarks",
|
||||
# Diğer paletlerin runtime-benchmarks özellikleri
|
||||
"pezpallet-balances/runtime-benchmarks",
|
||||
"pezpallet-identity-kyc/runtime-benchmarks",
|
||||
"pezpallet-perwerde/runtime-benchmarks",
|
||||
"pezpallet-referral/runtime-benchmarks",
|
||||
"pezpallet-staking-score/runtime-benchmarks",
|
||||
"pezpallet-tiki/runtime-benchmarks",
|
||||
"pezkuwi-primitives/runtime-benchmarks",
|
||||
"pezsp-core",
|
||||
"pezsp-io",
|
||||
"pezsp-io?/runtime-benchmarks",
|
||||
"pezsp-runtime/runtime-benchmarks",
|
||||
]
|
||||
try-runtime = [
|
||||
"pezframe-support/try-runtime",
|
||||
"pezframe-system/try-runtime",
|
||||
# Diğer paletlerin try-runtime özellikleri
|
||||
"pezpallet-balances/try-runtime",
|
||||
"pezpallet-identity-kyc/try-runtime",
|
||||
"pezpallet-perwerde/try-runtime",
|
||||
"pezpallet-referral/try-runtime",
|
||||
"pezpallet-staking-score/try-runtime",
|
||||
"pezpallet-tiki/try-runtime",
|
||||
"pezsp-runtime/try-runtime",
|
||||
]
|
||||
@@ -0,0 +1,72 @@
|
||||
//! Benchmarking setup for pezpallet-trust
|
||||
//!
|
||||
//! These benchmarks measure the performance of trust score operations.
|
||||
|
||||
#![cfg(feature = "runtime-benchmarks")]
|
||||
|
||||
use super::*;
|
||||
use crate::Pallet as TrustPallet;
|
||||
|
||||
use pezframe_benchmarking::{v2::*, whitelisted_caller};
|
||||
use pezframe_support::pezpallet_prelude::*;
|
||||
use pezframe_system::RawOrigin;
|
||||
use pezsp_runtime::traits::Zero;
|
||||
|
||||
// We don't use IdentityKycPallet directly - just mock the citizenship status
|
||||
// This simplifies benchmarks and avoids coupling with identity-kyc internals
|
||||
|
||||
#[benchmarks]
|
||||
mod benchmarks {
|
||||
use super::*;
|
||||
|
||||
/// Helper to setup a citizen for benchmarking
|
||||
/// Instead of calling identity-kyc extrinsics, we mock the citizenship source
|
||||
fn setup_citizen<T: Config>(account: &T::AccountId) {
|
||||
// For benchmarks, we rely on the runtime's CitizenshipSource implementation
|
||||
// The benchmark mock should configure CitizenshipSource to return true for whitelisted
|
||||
// accounts This is typically done via TestCitizenshipProvider in mock.rs
|
||||
|
||||
// Initialize trust score storage for the account so update operations work
|
||||
TrustScores::<T>::insert(account, T::Score::zero());
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn force_recalculate_trust_score() -> Result<(), BenchmarkError> {
|
||||
// Setup
|
||||
let account: T::AccountId = whitelisted_caller();
|
||||
setup_citizen::<T>(&account);
|
||||
|
||||
#[extrinsic_call]
|
||||
force_recalculate_trust_score(RawOrigin::Root, account.clone());
|
||||
|
||||
// Verify - trust score should be calculated (may be zero if no component scores)
|
||||
assert!(TrustScores::<T>::contains_key(&account));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn update_all_trust_scores() {
|
||||
// Setup - Ensure no batch update is in progress
|
||||
crate::BatchUpdateInProgress::<T>::put(false);
|
||||
|
||||
#[extrinsic_call]
|
||||
update_all_trust_scores(RawOrigin::Root);
|
||||
|
||||
// Verify - The function completed (may or may not have set BatchUpdateInProgress
|
||||
// depending on whether there are citizens to process)
|
||||
// We just verify it doesn't panic
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn periodic_trust_score_update() {
|
||||
// Setup - Ensure no batch update is in progress
|
||||
crate::BatchUpdateInProgress::<T>::put(false);
|
||||
|
||||
#[extrinsic_call]
|
||||
periodic_trust_score_update(RawOrigin::Root);
|
||||
|
||||
// Verify - The function completed successfully
|
||||
}
|
||||
|
||||
impl_benchmark_test_suite!(TrustPallet, crate::mock::new_test_ext(), crate::mock::Test);
|
||||
}
|
||||
@@ -0,0 +1,423 @@
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
//! # Trust Score Pallet
|
||||
//!
|
||||
//! A pallet for calculating and managing composite trust scores based on multiple ecosystem
|
||||
//! metrics.
|
||||
//!
|
||||
//! ## Overview
|
||||
//!
|
||||
//! The Trust Score pallet aggregates multiple reputation and activity metrics to produce
|
||||
//! a unified trust score for each citizen. This score is used throughout the ecosystem for:
|
||||
//!
|
||||
//! - Validator pool eligibility (trust-based validators)
|
||||
//! - Reward distribution weighting (pez-rewards)
|
||||
//! - Governance participation rights
|
||||
//! - Social reputation tracking
|
||||
//!
|
||||
//! ## Trust Score Components
|
||||
//!
|
||||
//! The trust score is calculated from four primary sources:
|
||||
//!
|
||||
//! 1. **Staking Score**: Economic security through token staking
|
||||
//! 2. **Referral Score**: Network growth contribution via referrals
|
||||
//! 3. **Perwerde Score**: Educational achievement and verification
|
||||
//! 4. **Tiki Score**: Social engagement and platform activity
|
||||
//!
|
||||
//! ## Score Calculation
|
||||
//!
|
||||
//! ```text
|
||||
//! trust_score = (staking_score + referral_score + perwerde_score + tiki_score) * multiplier
|
||||
//! ```
|
||||
//!
|
||||
//! Where:
|
||||
//! - Each component score is normalized and weighted
|
||||
//! - The multiplier is configurable via `ScoreMultiplierBase`
|
||||
//! - Citizenship status is required (KYC approved)
|
||||
//!
|
||||
//! ## Update Mechanisms
|
||||
//!
|
||||
//! ### Automatic Updates
|
||||
//! - Periodic batch updates scheduled at `UpdateInterval` (e.g., daily)
|
||||
//! - Processes all citizens in batches to manage computational load
|
||||
//! - Maintains update progress across blocks for large user bases
|
||||
//!
|
||||
//! ### Manual Updates
|
||||
//! - Individual score recalculation via privileged call
|
||||
//! - Full batch update trigger (root only)
|
||||
//! - Component change hooks from other pallets
|
||||
//!
|
||||
//! ## Storage
|
||||
//!
|
||||
//! - `TrustScores` - Per-account trust score mapping
|
||||
//! - `TotalActiveTrustScore` - Aggregate trust score across all citizens
|
||||
//! - `BatchUpdateInProgress` - Flag for ongoing batch update process
|
||||
//! - `LastProcessedAccount` - Checkpoint for resumable batch updates
|
||||
//!
|
||||
//! ## Interface
|
||||
//!
|
||||
//! ### Extrinsics
|
||||
//!
|
||||
//! - `force_recalculate_trust_score(who)` - Manually recalculate specific user's score (root)
|
||||
//! - `update_all_trust_scores()` - Trigger batch update of all citizens (root)
|
||||
//!
|
||||
//! ### Trait Implementations
|
||||
//!
|
||||
//! - `TrustScoreProvider` - Query trust scores from other pallets
|
||||
//! - `TrustScoreUpdater` - Receive notifications of component changes
|
||||
//!
|
||||
//! ## Dependencies
|
||||
//!
|
||||
//! This pallet requires integration with:
|
||||
//! - `pezpallet-identity-kyc` - Citizenship status verification
|
||||
//! - `pezpallet-staking-score` - Staking metrics provider
|
||||
//! - `pezpallet-referral` - Referral score provider
|
||||
//! - `pezpallet-perwerde` - Education score provider
|
||||
//! - `pezpallet-tiki` - Social engagement provider
|
||||
//!
|
||||
//! ## Runtime Integration Example
|
||||
//!
|
||||
//! ```ignore
|
||||
//! impl pezpallet_trust::Config for Runtime {
|
||||
//! type RuntimeEvent = RuntimeEvent;
|
||||
//! type WeightInfo = pezpallet_trust::weights::BizinikiwiWeight<Runtime>;
|
||||
//! type Score = u128;
|
||||
//! type ScoreMultiplierBase = ConstU128<100>;
|
||||
//! type UpdateInterval = ConstU32<14400>; // ~1 day in blocks
|
||||
//! type StakingScoreSource = StakingScore;
|
||||
//! type ReferralScoreSource = Referral;
|
||||
//! type PerwerdeScoreSource = Perwerde;
|
||||
//! type TikiScoreSource = Tiki;
|
||||
//! type CitizenshipSource = IdentityKyc;
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
pub use pallet::*;
|
||||
|
||||
pub mod weights;
|
||||
|
||||
#[cfg(test)]
|
||||
mod mock;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
mod benchmarking;
|
||||
|
||||
pub use pezpallet_staking_score::{RawScore as StakingRawScore, StakingScoreProvider};
|
||||
/* use pezkuwi_primitives::traits::{
|
||||
CitizenshipStatusProvider, PerwerdeScoreProvider, ReferralScoreProvider, RawScore,
|
||||
StakingDetails, StakingScoreProvider, TikiScoreProvider, TrustScoreUpdater, TrustScoreProvider
|
||||
}; */
|
||||
|
||||
use core::convert::TryFrom;
|
||||
use pezframe_system::pezpallet_prelude::BlockNumberFor;
|
||||
|
||||
use pezframe_support::pezpallet_prelude::{
|
||||
Get, IsType, MaxEncodedLen, Member, OptionQuery, Parameter, ValueQuery,
|
||||
};
|
||||
|
||||
pub trait ReferralScoreProvider<AccountId> {
|
||||
fn get_referral_score(who: &AccountId) -> u32;
|
||||
}
|
||||
|
||||
// Re-export from identity-kyc pallet
|
||||
pub use pezpallet_identity_kyc::CitizenshipStatusProvider;
|
||||
|
||||
pub trait TrustScoreUpdater<AccountId> {
|
||||
fn on_score_component_changed(who: &AccountId);
|
||||
}
|
||||
|
||||
pub trait PerwerdeScoreProvider<AccountId> {
|
||||
fn get_perwerde_score(who: &AccountId) -> u32;
|
||||
}
|
||||
|
||||
pub trait TrustScoreProvider<AccountId> {
|
||||
fn trust_score_of(who: &AccountId) -> u128;
|
||||
}
|
||||
|
||||
pub trait TikiScoreProvider<AccountId> {
|
||||
fn get_tiki_score(who: &AccountId) -> u32;
|
||||
}
|
||||
|
||||
#[pezframe_support::pallet]
|
||||
pub mod pallet {
|
||||
use super::{weights::WeightInfo, *};
|
||||
use pezframe_support::pezpallet_prelude::*;
|
||||
use pezframe_system::pezpallet_prelude::*;
|
||||
use pezsp_runtime::traits::{Saturating, Zero};
|
||||
|
||||
#[pallet::pallet]
|
||||
pub struct Pallet<T>(_);
|
||||
|
||||
#[pallet::config]
|
||||
pub trait Config: pezframe_system::Config + pezpallet_identity_kyc::Config {
|
||||
type RuntimeEvent: From<Event<Self>> + IsType<<Self as pezframe_system::Config>::RuntimeEvent>;
|
||||
type WeightInfo: WeightInfo;
|
||||
|
||||
type Score: Member
|
||||
+ Parameter
|
||||
+ MaxEncodedLen
|
||||
+ Copy
|
||||
+ Default
|
||||
+ PartialOrd
|
||||
+ Saturating
|
||||
+ Zero
|
||||
+ From<StakingRawScore>
|
||||
+ Into<u128>
|
||||
+ TryFrom<u128>;
|
||||
|
||||
#[pallet::constant]
|
||||
type ScoreMultiplierBase: Get<u128>;
|
||||
|
||||
/// Block interval for Trust score updates (e.g. daily)
|
||||
#[pallet::constant]
|
||||
type UpdateInterval: Get<BlockNumberFor<Self>>;
|
||||
|
||||
/// Maximum number of accounts to process per batch update
|
||||
/// Prevents DoS by limiting computation per extrinsic call
|
||||
#[pallet::constant]
|
||||
type MaxBatchSize: Get<u32>;
|
||||
|
||||
type StakingScoreSource: StakingScoreProvider<Self::AccountId, BlockNumberFor<Self>>;
|
||||
type ReferralScoreSource: ReferralScoreProvider<Self::AccountId>;
|
||||
type PerwerdeScoreSource: PerwerdeScoreProvider<Self::AccountId>;
|
||||
type TikiScoreSource: TikiScoreProvider<Self::AccountId>;
|
||||
type CitizenshipSource: CitizenshipStatusProvider<Self::AccountId>;
|
||||
}
|
||||
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn trust_score_of)]
|
||||
pub type TrustScores<T: Config> =
|
||||
StorageMap<_, Blake2_128Concat, T::AccountId, T::Score, ValueQuery>;
|
||||
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn total_active_trust_score)]
|
||||
pub type TotalActiveTrustScore<T: Config> = StorageValue<_, T::Score, ValueQuery>;
|
||||
|
||||
#[pallet::storage]
|
||||
pub type LastProcessedAccount<T: Config> = StorageValue<_, T::AccountId, OptionQuery>;
|
||||
|
||||
#[pallet::storage]
|
||||
pub type BatchUpdateInProgress<T: Config> = StorageValue<_, bool, ValueQuery>;
|
||||
|
||||
#[pallet::event]
|
||||
#[pallet::generate_deposit(pub(super) fn deposit_event)]
|
||||
pub enum Event<T: Config> {
|
||||
/// A user's Trust Score was successfully updated.
|
||||
TrustScoreUpdated { who: T::AccountId, old_score: T::Score, new_score: T::Score },
|
||||
/// Total active Trust Score on chain updated.
|
||||
TotalTrustScoreUpdated { new_total: T::Score },
|
||||
/// A batch Trust Score update completed.
|
||||
BulkTrustScoreUpdate { count: u32 },
|
||||
/// All Trust Scores update completed.
|
||||
AllTrustScoresUpdated { total_updated: u32 },
|
||||
/// Periodic Trust Score update scheduled for next time.
|
||||
PeriodicUpdateScheduled { next_block: BlockNumberFor<T> },
|
||||
}
|
||||
|
||||
#[pallet::error]
|
||||
#[derive(PartialEq)]
|
||||
pub enum Error<T> {
|
||||
CalculationOverflow,
|
||||
NotACitizen,
|
||||
UpdateInProgress,
|
||||
}
|
||||
|
||||
#[pallet::genesis_config]
|
||||
#[derive(pezframe_support::DefaultNoBound)]
|
||||
pub struct GenesisConfig<T: Config> {
|
||||
pub start_periodic_updates: bool,
|
||||
#[serde(skip)]
|
||||
pub _phantom: core::marker::PhantomData<T>,
|
||||
}
|
||||
|
||||
#[pallet::genesis_build]
|
||||
impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
|
||||
fn build(&self) {
|
||||
if self.start_periodic_updates {
|
||||
// Schedule first periodic update for 1 day later
|
||||
let _first_update_block =
|
||||
pezframe_system::Pallet::<T>::block_number() + T::UpdateInterval::get();
|
||||
|
||||
// Note: Scheduler may not be available during Genesis build
|
||||
// In this case, manual start required or scheduled in runtime
|
||||
// For now, we are just marking the flag
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[pallet::call]
|
||||
impl<T: Config> Pallet<T> {
|
||||
/// To manually recalculate a specific user's Trust Score.
|
||||
#[pallet::call_index(0)]
|
||||
#[pallet::weight(<T as Config>::WeightInfo::force_recalculate_trust_score())]
|
||||
pub fn force_recalculate_trust_score(
|
||||
origin: OriginFor<T>,
|
||||
who: T::AccountId,
|
||||
) -> DispatchResult {
|
||||
ensure_root(origin)?;
|
||||
Self::update_score_for_account(&who)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Updates Trust Scores of all citizens in bulk
|
||||
/// Works in batches for large user base using efficient pagination
|
||||
/// UPDATED (Gemini suggestion): Uses iter_from for true O(1) resume
|
||||
#[pallet::call_index(1)]
|
||||
#[pallet::weight(<T as Config>::WeightInfo::update_all_trust_scores())]
|
||||
pub fn update_all_trust_scores(origin: OriginFor<T>) -> DispatchResult {
|
||||
ensure_root(origin)?;
|
||||
|
||||
let batch_size = Self::calculate_optimal_batch_size();
|
||||
let mut updated_count = 0u32;
|
||||
let mut all_processed = true;
|
||||
let mut last_account: Option<T::AccountId> = None;
|
||||
|
||||
// Use iter_from for efficient pagination - O(1) resume instead of O(n) scan
|
||||
// This is critical for large user bases to prevent chain stalling
|
||||
let iterator = match LastProcessedAccount::<T>::get() {
|
||||
Some(start_key) => {
|
||||
// Resume from last processed account using iter_from
|
||||
pezpallet_identity_kyc::KycStatuses::<T>::iter_from(
|
||||
pezpallet_identity_kyc::KycStatuses::<T>::hashed_key_for(&start_key),
|
||||
)
|
||||
},
|
||||
None => {
|
||||
// Start from beginning
|
||||
pezpallet_identity_kyc::KycStatuses::<T>::iter()
|
||||
},
|
||||
};
|
||||
|
||||
// Process accounts in batch
|
||||
for (account, kyc_level) in iterator {
|
||||
// Is batch limit full?
|
||||
if updated_count >= batch_size {
|
||||
// Save last processed account for next batch
|
||||
last_account = Some(account);
|
||||
all_processed = false;
|
||||
break;
|
||||
}
|
||||
|
||||
// Only process accounts with Approved KYC (citizens)
|
||||
if kyc_level == pezpallet_identity_kyc::types::KycLevel::Approved {
|
||||
let _ = Self::update_score_for_account(&account);
|
||||
updated_count += 1;
|
||||
}
|
||||
|
||||
// Track last processed for checkpoint
|
||||
last_account = Some(account);
|
||||
}
|
||||
|
||||
// Update state based on completion
|
||||
if all_processed {
|
||||
LastProcessedAccount::<T>::kill();
|
||||
BatchUpdateInProgress::<T>::put(false);
|
||||
Self::deposit_event(Event::AllTrustScoresUpdated { total_updated: updated_count });
|
||||
} else {
|
||||
if let Some(ref account) = last_account {
|
||||
LastProcessedAccount::<T>::put(account.clone());
|
||||
}
|
||||
BatchUpdateInProgress::<T>::put(true);
|
||||
Self::deposit_event(Event::BulkTrustScoreUpdate { count: updated_count });
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Periyodik güncellemeyi başlatan function
|
||||
#[pallet::call_index(2)]
|
||||
#[pallet::weight(<T as Config>::WeightInfo::periodic_trust_score_update())]
|
||||
pub fn periodic_trust_score_update(origin: OriginFor<T>) -> DispatchResult {
|
||||
ensure_root(origin)?;
|
||||
|
||||
// Eğer önceki update devam ediyorsa bekle
|
||||
ensure!(!BatchUpdateInProgress::<T>::get(), Error::<T>::UpdateInProgress);
|
||||
|
||||
// Yeni periyodik güncellemeyi başlat
|
||||
Self::update_all_trust_scores(OriginFor::<T>::root())?;
|
||||
|
||||
// Bir sonraki periyodik güncellemeyi schedule et
|
||||
let current_block = pezframe_system::Pallet::<T>::block_number();
|
||||
let next_update_block = current_block + T::UpdateInterval::get();
|
||||
|
||||
Self::deposit_event(Event::PeriodicUpdateScheduled { next_block: next_update_block });
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> Pallet<T> {
|
||||
pub fn calculate_trust_score(who: &T::AccountId) -> Result<T::Score, Error<T>> {
|
||||
ensure!(T::CitizenshipSource::is_citizen(who), Error::<T>::NotACitizen);
|
||||
|
||||
let (staking_score_raw, _) = T::StakingScoreSource::get_staking_score(who);
|
||||
if staking_score_raw.is_zero() {
|
||||
return Ok(T::Score::zero());
|
||||
}
|
||||
|
||||
let staking_u128: u128 = staking_score_raw.into();
|
||||
let referral_u128: u128 = T::ReferralScoreSource::get_referral_score(who).into();
|
||||
let perwerde_u128: u128 = T::PerwerdeScoreSource::get_perwerde_score(who).into();
|
||||
let tiki_u128: u128 = T::TikiScoreSource::get_tiki_score(who).into();
|
||||
|
||||
let base = T::ScoreMultiplierBase::get();
|
||||
|
||||
let weighted_sum = staking_u128
|
||||
.saturating_mul(100)
|
||||
.saturating_add(referral_u128.saturating_mul(300))
|
||||
.saturating_add(perwerde_u128.saturating_mul(300))
|
||||
.saturating_add(tiki_u128.saturating_mul(300));
|
||||
|
||||
let final_score_u128 = staking_u128
|
||||
.saturating_mul(weighted_sum)
|
||||
.checked_div(base)
|
||||
.ok_or(Error::<T>::CalculationOverflow)?;
|
||||
|
||||
let new_trust_score = T::Score::try_from(final_score_u128)
|
||||
.map_err(|_| Error::<T>::CalculationOverflow)?;
|
||||
|
||||
Ok(new_trust_score)
|
||||
}
|
||||
|
||||
pub fn update_score_for_account(who: &T::AccountId) -> Result<T::Score, Error<T>> {
|
||||
let old_score = Self::trust_score_of(who);
|
||||
let new_score = Self::calculate_trust_score(who)?;
|
||||
|
||||
if old_score != new_score {
|
||||
<TrustScores<T>>::insert(who, new_score);
|
||||
let old_total = Self::total_active_trust_score();
|
||||
let new_total = old_total.saturating_sub(old_score).saturating_add(new_score);
|
||||
<TotalActiveTrustScore<T>>::put(new_total);
|
||||
Self::deposit_event(Event::TrustScoreUpdated {
|
||||
who: who.clone(),
|
||||
old_score,
|
||||
new_score,
|
||||
});
|
||||
Self::deposit_event(Event::TotalTrustScoreUpdated { new_total });
|
||||
}
|
||||
Ok(new_score)
|
||||
}
|
||||
|
||||
/// Returns the configured batch size for trust score updates
|
||||
/// Configurable via MaxBatchSize to allow governance control
|
||||
fn calculate_optimal_batch_size() -> u32 {
|
||||
T::MaxBatchSize::get()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> TrustScoreProvider<T::AccountId> for Pallet<T> {
|
||||
fn trust_score_of(who: &T::AccountId) -> u128 {
|
||||
Self::trust_score_of(who).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> TrustScoreUpdater<T::AccountId> for Pallet<T> {
|
||||
fn on_score_component_changed(who: &T::AccountId) {
|
||||
if let Err(e) = Self::update_score_for_account(who) {
|
||||
log::error!("Failed to update trust score for {:?}: {:?}", who, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
use crate as pezpallet_trust;
|
||||
use pezframe_support::{
|
||||
derive_impl, parameter_types,
|
||||
traits::{ConstU16, ConstU64},
|
||||
};
|
||||
use pezframe_system as system;
|
||||
use pezsp_core::H256;
|
||||
use pezsp_runtime::{
|
||||
traits::{BlakeTwo256, IdentityLookup},
|
||||
BuildStorage,
|
||||
};
|
||||
|
||||
type Block = pezframe_system::mocking::MockBlock<Test>;
|
||||
|
||||
pezframe_support::construct_runtime!(
|
||||
pub enum Test
|
||||
{
|
||||
System: pezframe_system,
|
||||
Balances: pezpallet_balances,
|
||||
IdentityKyc: pezpallet_identity_kyc,
|
||||
TrustPallet: pezpallet_trust,
|
||||
}
|
||||
);
|
||||
|
||||
#[derive_impl(pezframe_system::config_preludes::TestDefaultConfig as pezframe_system::DefaultConfig)]
|
||||
impl system::Config for Test {
|
||||
type BaseCallFilter = pezframe_support::traits::Everything;
|
||||
type BlockWeights = ();
|
||||
type BlockLength = ();
|
||||
type DbWeight = ();
|
||||
type RuntimeOrigin = RuntimeOrigin;
|
||||
type RuntimeCall = RuntimeCall;
|
||||
type Nonce = u64;
|
||||
type Hash = H256;
|
||||
type Hashing = BlakeTwo256;
|
||||
type AccountId = u64;
|
||||
type Lookup = IdentityLookup<Self::AccountId>;
|
||||
type Block = Block;
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type BlockHashCount = ConstU64<250>;
|
||||
type Version = ();
|
||||
type PalletInfo = PalletInfo;
|
||||
type AccountData = pezpallet_balances::AccountData<u128>;
|
||||
type OnNewAccount = ();
|
||||
type OnKilledAccount = ();
|
||||
type SystemWeightInfo = ();
|
||||
type SS58Prefix = ConstU16<42>;
|
||||
type OnSetCode = ();
|
||||
type MaxConsumers = pezframe_support::traits::ConstU32<16>;
|
||||
}
|
||||
|
||||
impl pezpallet_balances::Config for Test {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type WeightInfo = ();
|
||||
type Balance = u128;
|
||||
type DustRemoval = ();
|
||||
type ExistentialDeposit = pezframe_support::traits::ConstU128<1>;
|
||||
type AccountStore = System;
|
||||
type ReserveIdentifier = [u8; 8];
|
||||
type RuntimeHoldReason = ();
|
||||
type RuntimeFreezeReason = ();
|
||||
type FreezeIdentifier = ();
|
||||
type MaxLocks = pezframe_support::traits::ConstU32<10>;
|
||||
type MaxReserves = pezframe_support::traits::ConstU32<10>;
|
||||
type MaxFreezes = pezframe_support::traits::ConstU32<10>;
|
||||
type DoneSlashHandler = ();
|
||||
}
|
||||
|
||||
pub struct NoOpOnKycApproved;
|
||||
impl pezpallet_identity_kyc::types::OnKycApproved<u64> for NoOpOnKycApproved {
|
||||
fn on_kyc_approved(_who: &u64, _referrer: &u64) {}
|
||||
}
|
||||
|
||||
pub struct NoOpOnCitizenshipRevoked;
|
||||
impl pezpallet_identity_kyc::types::OnCitizenshipRevoked<u64> for NoOpOnCitizenshipRevoked {
|
||||
fn on_citizenship_revoked(_who: &u64) {}
|
||||
}
|
||||
|
||||
pub struct NoOpCitizenNftProvider;
|
||||
impl pezpallet_identity_kyc::types::CitizenNftProvider<u64> for NoOpCitizenNftProvider {
|
||||
fn mint_citizen_nft(_who: &u64) -> Result<(), pezsp_runtime::DispatchError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn mint_citizen_nft_confirmed(_who: &u64) -> Result<(), pezsp_runtime::DispatchError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn burn_citizen_nft(_who: &u64) -> Result<(), pezsp_runtime::DispatchError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl pezpallet_identity_kyc::Config for Test {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type Currency = Balances;
|
||||
type GovernanceOrigin = pezframe_system::EnsureRoot<u64>;
|
||||
type WeightInfo = ();
|
||||
type OnKycApproved = NoOpOnKycApproved;
|
||||
type OnCitizenshipRevoked = NoOpOnCitizenshipRevoked;
|
||||
type CitizenNftProvider = NoOpCitizenNftProvider;
|
||||
type KycApplicationDeposit = pezframe_support::traits::ConstU128<100>;
|
||||
type MaxStringLength = pezframe_support::traits::ConstU32<128>;
|
||||
type MaxCidLength = pezframe_support::traits::ConstU32<64>;
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const ScoreMultiplierBase: u128 = 1000;
|
||||
pub const TrustUpdateInterval: u64 = 100; // Test için kısa interval
|
||||
pub const MaxBatchSizeValue: u32 = 100; // Max users per batch
|
||||
}
|
||||
|
||||
pub struct MockStakingScoreProvider;
|
||||
impl pezpallet_trust::StakingScoreProvider<u64, u64> for MockStakingScoreProvider {
|
||||
fn get_staking_score(_who: &u64) -> (u32, u64) {
|
||||
(100, 0)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MockReferralScoreProvider;
|
||||
impl pezpallet_trust::ReferralScoreProvider<u64> for MockReferralScoreProvider {
|
||||
fn get_referral_score(_who: &u64) -> u32 {
|
||||
50
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MockPerwerdeScoreProvider;
|
||||
impl pezpallet_trust::PerwerdeScoreProvider<u64> for MockPerwerdeScoreProvider {
|
||||
fn get_perwerde_score(_who: &u64) -> u32 {
|
||||
30
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MockTikiScoreProvider;
|
||||
impl pezpallet_trust::TikiScoreProvider<u64> for MockTikiScoreProvider {
|
||||
fn get_tiki_score(_who: &u64) -> u32 {
|
||||
20
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MockCitizenshipStatusProvider;
|
||||
impl pezpallet_trust::CitizenshipStatusProvider<u64> for MockCitizenshipStatusProvider {
|
||||
fn is_citizen(who: &u64) -> bool {
|
||||
// Test için: 1-100 arası hesaplar vatandaş, 999 değil
|
||||
*who >= 1 && *who <= 100 && *who != 999
|
||||
}
|
||||
}
|
||||
|
||||
impl pezpallet_trust::Config for Test {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type WeightInfo = ();
|
||||
type Score = u128;
|
||||
type ScoreMultiplierBase = ScoreMultiplierBase;
|
||||
type UpdateInterval = TrustUpdateInterval;
|
||||
type MaxBatchSize = MaxBatchSizeValue;
|
||||
type StakingScoreSource = MockStakingScoreProvider;
|
||||
type ReferralScoreSource = MockReferralScoreProvider;
|
||||
type PerwerdeScoreSource = MockPerwerdeScoreProvider;
|
||||
type TikiScoreSource = MockTikiScoreProvider;
|
||||
type CitizenshipSource = MockCitizenshipStatusProvider;
|
||||
}
|
||||
|
||||
pub fn new_test_ext() -> pezsp_io::TestExternalities {
|
||||
system::GenesisConfig::<Test>::default().build_storage().unwrap().into()
|
||||
}
|
||||
@@ -0,0 +1,488 @@
|
||||
use crate::{mock::*, Error, Event};
|
||||
use pezframe_support::{assert_noop, assert_ok};
|
||||
use pezsp_runtime::traits::BadOrigin;
|
||||
|
||||
#[test]
|
||||
fn calculate_trust_score_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let account = 1u64;
|
||||
let score = TrustPallet::calculate_trust_score(&account).unwrap();
|
||||
|
||||
let expected = {
|
||||
let staking = 100u128;
|
||||
let referral = 50u128;
|
||||
let perwerde = 30u128;
|
||||
let tiki = 20u128;
|
||||
let base = ScoreMultiplierBase::get();
|
||||
|
||||
let weighted_sum = staking * 100 + referral * 300 + perwerde * 300 + tiki * 300;
|
||||
staking * weighted_sum / base
|
||||
};
|
||||
|
||||
assert_eq!(score, expected);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn calculate_trust_score_fails_for_non_citizen() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let non_citizen = 999u64;
|
||||
assert_noop!(TrustPallet::calculate_trust_score(&non_citizen), Error::<Test>::NotACitizen);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn calculate_trust_score_zero_staking() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let account = 1u64;
|
||||
let score = TrustPallet::calculate_trust_score(&account).unwrap();
|
||||
assert!(score > 0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_score_for_account_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let account = 1u64;
|
||||
|
||||
let initial_score = TrustPallet::trust_score_of(&account);
|
||||
assert_eq!(initial_score, 0);
|
||||
|
||||
let new_score = TrustPallet::update_score_for_account(&account).unwrap();
|
||||
assert!(new_score > 0);
|
||||
|
||||
let stored_score = TrustPallet::trust_score_of(&account);
|
||||
assert_eq!(stored_score, new_score);
|
||||
|
||||
let total_score = TrustPallet::total_active_trust_score();
|
||||
assert_eq!(total_score, new_score);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_score_for_account_updates_total() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let account1 = 1u64;
|
||||
let account2 = 2u64;
|
||||
|
||||
let score1 = TrustPallet::update_score_for_account(&account1).unwrap();
|
||||
let total_after_first = TrustPallet::total_active_trust_score();
|
||||
assert_eq!(total_after_first, score1);
|
||||
|
||||
let score2 = TrustPallet::update_score_for_account(&account2).unwrap();
|
||||
let total_after_second = TrustPallet::total_active_trust_score();
|
||||
assert_eq!(total_after_second, score1 + score2);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn force_recalculate_trust_score_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let account = 1u64;
|
||||
|
||||
assert_ok!(TrustPallet::force_recalculate_trust_score(RuntimeOrigin::root(), account));
|
||||
|
||||
let score = TrustPallet::trust_score_of(&account);
|
||||
assert!(score > 0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn force_recalculate_trust_score_requires_root() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let account = 1u64;
|
||||
|
||||
assert_noop!(
|
||||
TrustPallet::force_recalculate_trust_score(RuntimeOrigin::signed(account), account),
|
||||
BadOrigin
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_all_trust_scores_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// Event'leri yakalamak için block number set et
|
||||
System::set_block_number(1);
|
||||
|
||||
assert_ok!(TrustPallet::update_all_trust_scores(RuntimeOrigin::root()));
|
||||
|
||||
// Mock implementation boş account listesi kullandığı için
|
||||
// AllTrustScoresUpdated event'i yayınlanır (count: 0 ile)
|
||||
let events = System::events();
|
||||
assert!(events.iter().any(|event| {
|
||||
matches!(
|
||||
event.event,
|
||||
RuntimeEvent::TrustPallet(Event::AllTrustScoresUpdated { total_updated: 0 })
|
||||
)
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_all_trust_scores_requires_root() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_noop!(TrustPallet::update_all_trust_scores(RuntimeOrigin::signed(1)), BadOrigin);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn periodic_trust_score_update_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// Event'leri yakalamak için block number set et
|
||||
System::set_block_number(1);
|
||||
|
||||
assert_ok!(TrustPallet::periodic_trust_score_update(RuntimeOrigin::root()));
|
||||
|
||||
// Periyodik güncelleme event'inin yayınlandığını kontrol et
|
||||
let events = System::events();
|
||||
assert!(events.iter().any(|event| {
|
||||
matches!(event.event, RuntimeEvent::TrustPallet(Event::PeriodicUpdateScheduled { .. }))
|
||||
}));
|
||||
|
||||
// Ayrıca AllTrustScoresUpdated event'i de yayınlanmalı
|
||||
assert!(events.iter().any(|event| {
|
||||
matches!(event.event, RuntimeEvent::TrustPallet(Event::AllTrustScoresUpdated { .. }))
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn periodic_update_fails_when_batch_in_progress() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// Batch update'i başlat
|
||||
crate::BatchUpdateInProgress::<Test>::put(true);
|
||||
|
||||
// Periyodik update'in başarısız olmasını bekle
|
||||
assert_noop!(
|
||||
TrustPallet::periodic_trust_score_update(RuntimeOrigin::root()),
|
||||
Error::<Test>::UpdateInProgress
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn events_are_emitted() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let account = 1u64;
|
||||
|
||||
System::set_block_number(1);
|
||||
|
||||
TrustPallet::update_score_for_account(&account).unwrap();
|
||||
|
||||
let events = System::events();
|
||||
assert!(events.len() >= 2);
|
||||
|
||||
let trust_score_updated = events.iter().any(|event| {
|
||||
matches!(event.event, RuntimeEvent::TrustPallet(Event::TrustScoreUpdated { .. }))
|
||||
});
|
||||
|
||||
let total_updated = events.iter().any(|event| {
|
||||
matches!(event.event, RuntimeEvent::TrustPallet(Event::TotalTrustScoreUpdated { .. }))
|
||||
});
|
||||
|
||||
assert!(trust_score_updated);
|
||||
assert!(total_updated);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trust_score_updater_trait_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
use crate::TrustScoreUpdater;
|
||||
|
||||
let account = 1u64;
|
||||
|
||||
let initial_score = TrustPallet::trust_score_of(&account);
|
||||
assert_eq!(initial_score, 0);
|
||||
|
||||
TrustPallet::on_score_component_changed(&account);
|
||||
|
||||
let updated_score = TrustPallet::trust_score_of(&account);
|
||||
assert!(updated_score > 0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_update_storage_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// Başlangıçta batch update aktif değil
|
||||
assert!(!crate::BatchUpdateInProgress::<Test>::get());
|
||||
assert!(crate::LastProcessedAccount::<Test>::get().is_none());
|
||||
|
||||
// Batch update'i simüle et
|
||||
crate::BatchUpdateInProgress::<Test>::put(true);
|
||||
crate::LastProcessedAccount::<Test>::put(42u64);
|
||||
|
||||
assert!(crate::BatchUpdateInProgress::<Test>::get());
|
||||
assert_eq!(crate::LastProcessedAccount::<Test>::get(), Some(42u64));
|
||||
|
||||
// Temizle
|
||||
crate::BatchUpdateInProgress::<Test>::put(false);
|
||||
crate::LastProcessedAccount::<Test>::kill();
|
||||
|
||||
assert!(!crate::BatchUpdateInProgress::<Test>::get());
|
||||
assert!(crate::LastProcessedAccount::<Test>::get().is_none());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn periodic_update_scheduling_works() {
|
||||
new_test_ext().execute_with(|| {
|
||||
System::set_block_number(100);
|
||||
|
||||
assert_ok!(TrustPallet::periodic_trust_score_update(RuntimeOrigin::root()));
|
||||
|
||||
// Event'te next_block'un doğru hesaplandığını kontrol et
|
||||
let events = System::events();
|
||||
let scheduled_event = events.iter().find(|event| {
|
||||
matches!(event.event, RuntimeEvent::TrustPallet(Event::PeriodicUpdateScheduled { .. }))
|
||||
});
|
||||
|
||||
assert!(scheduled_event.is_some());
|
||||
|
||||
if let Some(event_record) = scheduled_event {
|
||||
if let RuntimeEvent::TrustPallet(Event::PeriodicUpdateScheduled { next_block }) =
|
||||
&event_record.event
|
||||
{
|
||||
// Current block (100) + interval (100) = 200
|
||||
assert_eq!(next_block, &200u64);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// update_all_trust_scores Tests (5 tests)
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn update_all_trust_scores_multiple_users() {
|
||||
new_test_ext().execute_with(|| {
|
||||
System::set_block_number(1);
|
||||
|
||||
// Root can update all trust scores
|
||||
assert_ok!(TrustPallet::update_all_trust_scores(RuntimeOrigin::root()));
|
||||
|
||||
// Verify at least one user has score (depends on mock KYC setup)
|
||||
let total = TrustPallet::total_active_trust_score();
|
||||
assert!(total >= 0); // May be 0 if no users have KYC approved in mock
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_all_trust_scores_root_only() {
|
||||
new_test_ext().execute_with(|| {
|
||||
// Non-root cannot update all trust scores
|
||||
assert_noop!(TrustPallet::update_all_trust_scores(RuntimeOrigin::signed(1)), BadOrigin);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_all_trust_scores_updates_total() {
|
||||
new_test_ext().execute_with(|| {
|
||||
System::set_block_number(1);
|
||||
|
||||
let initial_total = TrustPallet::total_active_trust_score();
|
||||
assert_eq!(initial_total, 0);
|
||||
|
||||
assert_ok!(TrustPallet::update_all_trust_scores(RuntimeOrigin::root()));
|
||||
|
||||
let final_total = TrustPallet::total_active_trust_score();
|
||||
// Total should remain valid (may stay 0 if no approved KYC users)
|
||||
assert!(final_total >= 0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_all_trust_scores_emits_event() {
|
||||
new_test_ext().execute_with(|| {
|
||||
System::set_block_number(1);
|
||||
|
||||
assert_ok!(TrustPallet::update_all_trust_scores(RuntimeOrigin::root()));
|
||||
|
||||
let events = System::events();
|
||||
let bulk_update_event = events.iter().any(|event| {
|
||||
matches!(event.event, RuntimeEvent::TrustPallet(Event::BulkTrustScoreUpdate { .. })) ||
|
||||
matches!(
|
||||
event.event,
|
||||
RuntimeEvent::TrustPallet(Event::AllTrustScoresUpdated { .. })
|
||||
)
|
||||
});
|
||||
|
||||
assert!(bulk_update_event);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_all_trust_scores_batch_processing() {
|
||||
new_test_ext().execute_with(|| {
|
||||
System::set_block_number(1);
|
||||
|
||||
// First call should start batch processing
|
||||
assert_ok!(TrustPallet::update_all_trust_scores(RuntimeOrigin::root()));
|
||||
|
||||
// Check batch state is cleared after completion
|
||||
assert!(!crate::BatchUpdateInProgress::<Test>::get());
|
||||
assert!(crate::LastProcessedAccount::<Test>::get().is_none());
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Score Calculation Edge Cases (5 tests)
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn calculate_trust_score_handles_overflow() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let account = 1u64;
|
||||
|
||||
// Even with large values, should not overflow
|
||||
let score = TrustPallet::calculate_trust_score(&account);
|
||||
assert!(score.is_ok());
|
||||
assert!(score.unwrap() < u128::MAX);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn calculate_trust_score_all_zero_components() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let account = 2u64; // User 2 exists in mock
|
||||
|
||||
let score = TrustPallet::calculate_trust_score(&account).unwrap();
|
||||
// Should be greater than 0 (mock provides some values)
|
||||
assert!(score >= 0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_score_maintains_consistency() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let account = 1u64;
|
||||
|
||||
// Update twice
|
||||
let score1 = TrustPallet::update_score_for_account(&account).unwrap();
|
||||
let score2 = TrustPallet::update_score_for_account(&account).unwrap();
|
||||
|
||||
// Scores should be equal (no random component)
|
||||
assert_eq!(score1, score2);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trust_score_decreases_when_components_decrease() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let account = 1u64;
|
||||
|
||||
// First update with good scores
|
||||
let initial_score = TrustPallet::update_score_for_account(&account).unwrap();
|
||||
|
||||
// Simulate component decrease (in real scenario, staking/referral would decrease)
|
||||
// For now, just verify score can be recalculated
|
||||
let recalculated = TrustPallet::calculate_trust_score(&account).unwrap();
|
||||
|
||||
// Score should be deterministic
|
||||
assert_eq!(initial_score, recalculated);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_users_independent_scores() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let user1 = 1u64;
|
||||
let user2 = 2u64;
|
||||
|
||||
let score1 = TrustPallet::update_score_for_account(&user1).unwrap();
|
||||
let score2 = TrustPallet::update_score_for_account(&user2).unwrap();
|
||||
|
||||
// Scores should be independent
|
||||
assert_ne!(score1, 0);
|
||||
assert_ne!(score2, 0);
|
||||
|
||||
// Verify stored separately
|
||||
assert_eq!(TrustPallet::trust_score_of(&user1), score1);
|
||||
assert_eq!(TrustPallet::trust_score_of(&user2), score2);
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TrustScoreProvider Trait Tests (3 tests)
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn trust_score_provider_trait_returns_zero_initially() {
|
||||
new_test_ext().execute_with(|| {
|
||||
use crate::TrustScoreProvider;
|
||||
|
||||
let account = 1u64;
|
||||
let score = TrustPallet::trust_score_of(&account);
|
||||
assert_eq!(score, 0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trust_score_provider_trait_returns_updated_score() {
|
||||
new_test_ext().execute_with(|| {
|
||||
use crate::TrustScoreProvider;
|
||||
|
||||
let account = 1u64;
|
||||
TrustPallet::update_score_for_account(&account).unwrap();
|
||||
|
||||
let score = TrustPallet::trust_score_of(&account);
|
||||
assert!(score > 0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trust_score_provider_trait_multiple_users() {
|
||||
new_test_ext().execute_with(|| {
|
||||
use crate::TrustScoreProvider;
|
||||
|
||||
TrustPallet::update_score_for_account(&1u64).unwrap();
|
||||
TrustPallet::update_score_for_account(&2u64).unwrap();
|
||||
|
||||
let score1 = TrustPallet::trust_score_of(&1u64);
|
||||
let score2 = TrustPallet::trust_score_of(&2u64);
|
||||
|
||||
assert!(score1 > 0);
|
||||
assert!(score2 > 0);
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Storage and State Tests (2 tests)
|
||||
// ============================================================================
|
||||
|
||||
#[test]
|
||||
fn storage_consistency_after_multiple_updates() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let account = 1u64;
|
||||
|
||||
// Multiple updates
|
||||
for _ in 0..5 {
|
||||
TrustPallet::update_score_for_account(&account).unwrap();
|
||||
}
|
||||
|
||||
// Score should still be consistent
|
||||
let stored = TrustPallet::trust_score_of(&account);
|
||||
let calculated = TrustPallet::calculate_trust_score(&account).unwrap();
|
||||
|
||||
assert_eq!(stored, calculated);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn total_active_trust_score_accumulates_correctly() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let users = vec![1u64, 2u64]; // Only users that exist in mock
|
||||
let mut expected_total = 0u128;
|
||||
|
||||
for user in users {
|
||||
let score = TrustPallet::update_score_for_account(&user).unwrap();
|
||||
expected_total += score;
|
||||
}
|
||||
|
||||
let total = TrustPallet::total_active_trust_score();
|
||||
assert_eq!(total, expected_total);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
|
||||
//! Autogenerated weights for `pezpallet_trust`
|
||||
//!
|
||||
//! THIS FILE WAS AUTO-GENERATED USING THE BIZINIKIWI BENCHMARK CLI VERSION 32.0.0
|
||||
//! DATE: 2025-12-08, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
|
||||
//! WORST CASE MAP SIZE: `1000000`
|
||||
//! HOSTNAME: `MamostePC`, CPU: `11th Gen Intel(R) Core(TM) i9-11950H @ 2.60GHz`
|
||||
//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: `1024`
|
||||
|
||||
// Executed Command:
|
||||
// ./target/release/frame-omni-bencher
|
||||
// v1
|
||||
// benchmark
|
||||
// pallet
|
||||
// --runtime
|
||||
// target/release/wbuild/people-pezkuwichain-runtime/people_pezkuwichain_runtime.compact.compressed.wasm
|
||||
// --pallets
|
||||
// pezpallet_trust
|
||||
// -e
|
||||
// all
|
||||
// --steps
|
||||
// 50
|
||||
// --repeat
|
||||
// 20
|
||||
// --output
|
||||
// pezcumulus/teyrchains/pallets/trust/src/weights.rs
|
||||
// --template
|
||||
// bizinikiwi/.maintain/frame-weight-template.hbs
|
||||
|
||||
#![cfg_attr(rustfmt, rustfmt_skip)]
|
||||
#![allow(unused_parens)]
|
||||
#![allow(unused_imports)]
|
||||
#![allow(missing_docs)]
|
||||
#![allow(dead_code)]
|
||||
|
||||
use pezframe_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}};
|
||||
use core::marker::PhantomData;
|
||||
|
||||
/// Weight functions needed for `pezpallet_trust`.
|
||||
pub trait WeightInfo {
|
||||
fn force_recalculate_trust_score() -> Weight;
|
||||
fn update_all_trust_scores() -> Weight;
|
||||
fn periodic_trust_score_update() -> Weight;
|
||||
}
|
||||
|
||||
/// Weights for `pezpallet_trust` using the Bizinikiwi node and recommended hardware.
|
||||
pub struct BizinikiwiWeight<T>(PhantomData<T>);
|
||||
impl<T: pezframe_system::Config> WeightInfo for BizinikiwiWeight<T> {
|
||||
/// Storage: `Trust::TrustScores` (r:1 w:1)
|
||||
/// Proof: `Trust::TrustScores` (`max_values`: None, `max_size`: Some(64), added: 2539, mode: `MaxEncodedLen`)
|
||||
/// Storage: `StakingScore::StakingStartBlock` (r:1 w:0)
|
||||
/// Proof: `StakingScore::StakingStartBlock` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Referral::ReferralCount` (r:1 w:0)
|
||||
/// Proof: `Referral::ReferralCount` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Tiki::UserTikis` (r:1 w:0)
|
||||
/// Proof: `Tiki::UserTikis` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Trust::TotalActiveTrustScore` (r:1 w:1)
|
||||
/// Proof: `Trust::TotalActiveTrustScore` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`)
|
||||
fn force_recalculate_trust_score() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `287`
|
||||
// Estimated: `3534`
|
||||
// Minimum execution time: 41_676_000 picoseconds.
|
||||
Weight::from_parts(51_361_000, 3534)
|
||||
.saturating_add(T::DbWeight::get().reads(5_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(2_u64))
|
||||
}
|
||||
/// Storage: `Trust::LastProcessedAccount` (r:1 w:1)
|
||||
/// Proof: `Trust::LastProcessedAccount` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`)
|
||||
/// Storage: `IdentityKyc::KycStatuses` (r:2 w:0)
|
||||
/// Proof: `IdentityKyc::KycStatuses` (`max_values`: None, `max_size`: Some(49), added: 2524, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Trust::TrustScores` (r:1 w:1)
|
||||
/// Proof: `Trust::TrustScores` (`max_values`: None, `max_size`: Some(64), added: 2539, mode: `MaxEncodedLen`)
|
||||
/// Storage: `StakingScore::StakingStartBlock` (r:1 w:0)
|
||||
/// Proof: `StakingScore::StakingStartBlock` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Referral::ReferralCount` (r:1 w:0)
|
||||
/// Proof: `Referral::ReferralCount` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Tiki::UserTikis` (r:1 w:0)
|
||||
/// Proof: `Tiki::UserTikis` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Trust::TotalActiveTrustScore` (r:1 w:1)
|
||||
/// Proof: `Trust::TotalActiveTrustScore` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Trust::BatchUpdateInProgress` (r:0 w:1)
|
||||
/// Proof: `Trust::BatchUpdateInProgress` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`)
|
||||
fn update_all_trust_scores() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `338`
|
||||
// Estimated: `6038`
|
||||
// Minimum execution time: 57_062_000 picoseconds.
|
||||
Weight::from_parts(71_311_000, 6038)
|
||||
.saturating_add(T::DbWeight::get().reads(8_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(4_u64))
|
||||
}
|
||||
/// Storage: `Trust::BatchUpdateInProgress` (r:1 w:1)
|
||||
/// Proof: `Trust::BatchUpdateInProgress` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Trust::LastProcessedAccount` (r:1 w:1)
|
||||
/// Proof: `Trust::LastProcessedAccount` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`)
|
||||
/// Storage: `IdentityKyc::KycStatuses` (r:2 w:0)
|
||||
/// Proof: `IdentityKyc::KycStatuses` (`max_values`: None, `max_size`: Some(49), added: 2524, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Trust::TrustScores` (r:1 w:1)
|
||||
/// Proof: `Trust::TrustScores` (`max_values`: None, `max_size`: Some(64), added: 2539, mode: `MaxEncodedLen`)
|
||||
/// Storage: `StakingScore::StakingStartBlock` (r:1 w:0)
|
||||
/// Proof: `StakingScore::StakingStartBlock` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Referral::ReferralCount` (r:1 w:0)
|
||||
/// Proof: `Referral::ReferralCount` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Tiki::UserTikis` (r:1 w:0)
|
||||
/// Proof: `Tiki::UserTikis` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Trust::TotalActiveTrustScore` (r:1 w:1)
|
||||
/// Proof: `Trust::TotalActiveTrustScore` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`)
|
||||
fn periodic_trust_score_update() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `338`
|
||||
// Estimated: `6038`
|
||||
// Minimum execution time: 82_604_000 picoseconds.
|
||||
Weight::from_parts(88_810_000, 6038)
|
||||
.saturating_add(T::DbWeight::get().reads(9_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(4_u64))
|
||||
}
|
||||
}
|
||||
|
||||
// For backwards compatibility and tests.
|
||||
impl WeightInfo for () {
|
||||
/// Storage: `Trust::TrustScores` (r:1 w:1)
|
||||
/// Proof: `Trust::TrustScores` (`max_values`: None, `max_size`: Some(64), added: 2539, mode: `MaxEncodedLen`)
|
||||
/// Storage: `StakingScore::StakingStartBlock` (r:1 w:0)
|
||||
/// Proof: `StakingScore::StakingStartBlock` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Referral::ReferralCount` (r:1 w:0)
|
||||
/// Proof: `Referral::ReferralCount` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Tiki::UserTikis` (r:1 w:0)
|
||||
/// Proof: `Tiki::UserTikis` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Trust::TotalActiveTrustScore` (r:1 w:1)
|
||||
/// Proof: `Trust::TotalActiveTrustScore` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`)
|
||||
fn force_recalculate_trust_score() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `287`
|
||||
// Estimated: `3534`
|
||||
// Minimum execution time: 41_676_000 picoseconds.
|
||||
Weight::from_parts(51_361_000, 3534)
|
||||
.saturating_add(RocksDbWeight::get().reads(5_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(2_u64))
|
||||
}
|
||||
/// Storage: `Trust::LastProcessedAccount` (r:1 w:1)
|
||||
/// Proof: `Trust::LastProcessedAccount` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`)
|
||||
/// Storage: `IdentityKyc::KycStatuses` (r:2 w:0)
|
||||
/// Proof: `IdentityKyc::KycStatuses` (`max_values`: None, `max_size`: Some(49), added: 2524, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Trust::TrustScores` (r:1 w:1)
|
||||
/// Proof: `Trust::TrustScores` (`max_values`: None, `max_size`: Some(64), added: 2539, mode: `MaxEncodedLen`)
|
||||
/// Storage: `StakingScore::StakingStartBlock` (r:1 w:0)
|
||||
/// Proof: `StakingScore::StakingStartBlock` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Referral::ReferralCount` (r:1 w:0)
|
||||
/// Proof: `Referral::ReferralCount` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Tiki::UserTikis` (r:1 w:0)
|
||||
/// Proof: `Tiki::UserTikis` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Trust::TotalActiveTrustScore` (r:1 w:1)
|
||||
/// Proof: `Trust::TotalActiveTrustScore` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Trust::BatchUpdateInProgress` (r:0 w:1)
|
||||
/// Proof: `Trust::BatchUpdateInProgress` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`)
|
||||
fn update_all_trust_scores() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `338`
|
||||
// Estimated: `6038`
|
||||
// Minimum execution time: 57_062_000 picoseconds.
|
||||
Weight::from_parts(71_311_000, 6038)
|
||||
.saturating_add(RocksDbWeight::get().reads(8_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(4_u64))
|
||||
}
|
||||
/// Storage: `Trust::BatchUpdateInProgress` (r:1 w:1)
|
||||
/// Proof: `Trust::BatchUpdateInProgress` (`max_values`: Some(1), `max_size`: Some(1), added: 496, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Trust::LastProcessedAccount` (r:1 w:1)
|
||||
/// Proof: `Trust::LastProcessedAccount` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`)
|
||||
/// Storage: `IdentityKyc::KycStatuses` (r:2 w:0)
|
||||
/// Proof: `IdentityKyc::KycStatuses` (`max_values`: None, `max_size`: Some(49), added: 2524, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Trust::TrustScores` (r:1 w:1)
|
||||
/// Proof: `Trust::TrustScores` (`max_values`: None, `max_size`: Some(64), added: 2539, mode: `MaxEncodedLen`)
|
||||
/// Storage: `StakingScore::StakingStartBlock` (r:1 w:0)
|
||||
/// Proof: `StakingScore::StakingStartBlock` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Referral::ReferralCount` (r:1 w:0)
|
||||
/// Proof: `Referral::ReferralCount` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Tiki::UserTikis` (r:1 w:0)
|
||||
/// Proof: `Tiki::UserTikis` (`max_values`: None, `max_size`: Some(69), added: 2544, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Trust::TotalActiveTrustScore` (r:1 w:1)
|
||||
/// Proof: `Trust::TotalActiveTrustScore` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`)
|
||||
fn periodic_trust_score_update() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `338`
|
||||
// Estimated: `6038`
|
||||
// Minimum execution time: 82_604_000 picoseconds.
|
||||
Weight::from_parts(88_810_000, 6038)
|
||||
.saturating_add(RocksDbWeight::get().reads(9_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(4_u64))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
[package]
|
||||
name = "pezpallet-welati"
|
||||
version = "1.0.0"
|
||||
description = "PezkuwiChain Governance and State Administration Pallet"
|
||||
authors.workspace = true
|
||||
homepage.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
publish = false
|
||||
repository.workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
||||
[dependencies]
|
||||
codec = { workspace = true, default-features = false, features = [
|
||||
"derive",
|
||||
"max-encoded-len",
|
||||
] }
|
||||
scale-info = { default-features = false, features = [
|
||||
"derive",
|
||||
], workspace = true }
|
||||
serde = { version = "1.0", default-features = false, features = [
|
||||
"derive",
|
||||
], optional = true }
|
||||
|
||||
pezframe-benchmarking = { optional = true, workspace = true }
|
||||
pezframe-support = { default-features = false, workspace = true }
|
||||
pezframe-system = { default-features = false, workspace = true }
|
||||
log = { default-features = false, workspace = true }
|
||||
pezsp-core = { workspace = true, default-features = false, optional = true }
|
||||
pezsp-io = { workspace = true, default-features = false, optional = true }
|
||||
pezsp-runtime = { default-features = false, workspace = true }
|
||||
pezsp-std = { default-features = false, workspace = true }
|
||||
|
||||
# PezkuwiChain özel paletler
|
||||
pezpallet-identity-kyc = { workspace = true, default-features = false }
|
||||
pezpallet-referral = { workspace = true, default-features = false }
|
||||
pezpallet-tiki = { workspace = true, default-features = false }
|
||||
pezpallet-trust = { workspace = true, default-features = false }
|
||||
|
||||
# Bizinikiwi core paletler
|
||||
pezpallet-collective = { default-features = false, workspace = true }
|
||||
pezpallet-democracy = { default-features = false, workspace = true }
|
||||
pezpallet-elections-phragmen = { default-features = false, workspace = true }
|
||||
pezpallet-scheduler = { default-features = false, workspace = true }
|
||||
pezpallet-timestamp = { default-features = false, workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
pezpallet-balances = { workspace = true, default-features = false }
|
||||
pezpallet-identity = { workspace = true, default-features = false }
|
||||
pezpallet-nfts = { workspace = true, default-features = false }
|
||||
pezpallet-staking-score = { workspace = true, default-features = false }
|
||||
pezsp-core = { workspace = true, default-features = false }
|
||||
pezsp-io = { workspace = true, default-features = false }
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = [
|
||||
"codec/std",
|
||||
"pezframe-benchmarking?/std",
|
||||
"pezframe-support/std",
|
||||
"pezframe-system/std",
|
||||
"log/std",
|
||||
"pezpallet-balances/std",
|
||||
"pezpallet-collective/std",
|
||||
"pezpallet-democracy/std",
|
||||
"pezpallet-elections-phragmen/std",
|
||||
"pezpallet-identity-kyc/std",
|
||||
"pezpallet-identity/std",
|
||||
"pezpallet-nfts/std",
|
||||
"pezpallet-referral/std",
|
||||
"pezpallet-scheduler/std",
|
||||
"pezpallet-staking-score/std",
|
||||
"pezpallet-tiki/std",
|
||||
"pezpallet-timestamp/std",
|
||||
"pezpallet-trust/std",
|
||||
"scale-info/std",
|
||||
"serde",
|
||||
"serde?/std",
|
||||
"pezsp-core/std",
|
||||
"pezsp-io/std",
|
||||
"pezsp-runtime/std",
|
||||
"pezsp-std/std",
|
||||
]
|
||||
runtime-benchmarks = [
|
||||
"pezframe-benchmarking/runtime-benchmarks",
|
||||
"pezframe-support/runtime-benchmarks",
|
||||
"pezframe-system/runtime-benchmarks",
|
||||
"pezpallet-balances/runtime-benchmarks",
|
||||
"pezpallet-collective/runtime-benchmarks",
|
||||
"pezpallet-democracy/runtime-benchmarks",
|
||||
"pezpallet-elections-phragmen/runtime-benchmarks",
|
||||
"pezpallet-identity-kyc/runtime-benchmarks",
|
||||
"pezpallet-identity/runtime-benchmarks",
|
||||
"pezpallet-nfts/runtime-benchmarks",
|
||||
"pezpallet-referral/runtime-benchmarks",
|
||||
"pezpallet-scheduler/runtime-benchmarks",
|
||||
"pezpallet-staking-score/runtime-benchmarks",
|
||||
"pezpallet-tiki/runtime-benchmarks",
|
||||
"pezpallet-timestamp/runtime-benchmarks",
|
||||
"pezpallet-trust/runtime-benchmarks",
|
||||
"pezsp-core",
|
||||
"pezsp-io",
|
||||
"pezsp-io?/runtime-benchmarks",
|
||||
"pezsp-runtime/runtime-benchmarks",
|
||||
]
|
||||
try-runtime = [
|
||||
"pezframe-support/try-runtime",
|
||||
"pezframe-system/try-runtime",
|
||||
"pezpallet-balances/try-runtime",
|
||||
"pezpallet-collective/try-runtime",
|
||||
"pezpallet-democracy/try-runtime",
|
||||
"pezpallet-elections-phragmen/try-runtime",
|
||||
"pezpallet-identity-kyc/try-runtime",
|
||||
"pezpallet-identity/try-runtime",
|
||||
"pezpallet-nfts/try-runtime",
|
||||
"pezpallet-referral/try-runtime",
|
||||
"pezpallet-scheduler/try-runtime",
|
||||
"pezpallet-staking-score/try-runtime",
|
||||
"pezpallet-tiki/try-runtime",
|
||||
"pezpallet-timestamp/try-runtime",
|
||||
"pezpallet-trust/try-runtime",
|
||||
"pezsp-runtime/try-runtime",
|
||||
]
|
||||
@@ -0,0 +1,305 @@
|
||||
#![cfg(feature = "runtime-benchmarks")]
|
||||
|
||||
use super::*;
|
||||
use crate::types::*;
|
||||
use pezframe_benchmarking::v2::*;
|
||||
use pezframe_system::RawOrigin;
|
||||
|
||||
#[benchmarks]
|
||||
mod benchmarks {
|
||||
use super::*;
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// ELECTION SYSTEM BENCHMARKS
|
||||
// ----------------------------------------------------------------
|
||||
#[benchmark]
|
||||
fn initiate_election() {
|
||||
// This benchmark doesn't need special preparation, just needs to be called with root
|
||||
|
||||
#[extrinsic_call]
|
||||
initiate_election(RawOrigin::Root, ElectionType::Parliamentary, None, None);
|
||||
|
||||
assert!(ActiveElections::<T>::get(0).is_some());
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn register_candidate() {
|
||||
// --- SETUP ---
|
||||
Pallet::<T>::initiate_election(
|
||||
RawOrigin::Root.into(),
|
||||
ElectionType::Parliamentary,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Simplified endorsers for benchmark - KYC bypass
|
||||
let endorsers: Vec<T::AccountId> = (0..T::ParliamentaryEndorsements::get())
|
||||
.map(|i| account("endorser", i, 0))
|
||||
.collect();
|
||||
|
||||
let new_candidate: T::AccountId = whitelisted_caller();
|
||||
|
||||
// KYC check is already bypassed in test environment
|
||||
|
||||
#[extrinsic_call]
|
||||
register_candidate(RawOrigin::Signed(new_candidate.clone()), 0, None, endorsers);
|
||||
|
||||
assert!(ElectionCandidates::<T>::get(0, &new_candidate).is_some());
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn cast_vote() {
|
||||
// --- SETUP ---
|
||||
// 1. Prepare election and candidates
|
||||
Pallet::<T>::initiate_election(
|
||||
RawOrigin::Root.into(),
|
||||
ElectionType::Parliamentary,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let candidate: T::AccountId = account("candidate", 1, 0);
|
||||
let voter: T::AccountId = whitelisted_caller();
|
||||
|
||||
// Simplified endorsers for benchmark
|
||||
let endorsers: Vec<T::AccountId> = (0..T::ParliamentaryEndorsements::get())
|
||||
.map(|i| account("endorser", i, 0))
|
||||
.collect();
|
||||
|
||||
// KYC check is already bypassed in test environment
|
||||
|
||||
Pallet::<T>::register_candidate(
|
||||
RawOrigin::Signed(candidate.clone()).into(),
|
||||
0,
|
||||
None,
|
||||
endorsers,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// 2. Advance to voting period
|
||||
let election = ActiveElections::<T>::get(0).unwrap();
|
||||
pezframe_system::Pallet::<T>::set_block_number(election.voting_start);
|
||||
|
||||
let candidates_to_vote_for = vec![candidate];
|
||||
|
||||
#[extrinsic_call]
|
||||
cast_vote(RawOrigin::Signed(voter.clone()), 0, candidates_to_vote_for, None);
|
||||
|
||||
assert!(ElectionVotes::<T>::get(0, &voter).is_some());
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn finalize_election() {
|
||||
// --- SETUP ---
|
||||
// 1. Prepare election, candidate and a vote
|
||||
Pallet::<T>::initiate_election(
|
||||
RawOrigin::Root.into(),
|
||||
ElectionType::Parliamentary,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let candidate: T::AccountId = account("candidate", 1, 0);
|
||||
let voter: T::AccountId = account("voter", 2, 0);
|
||||
|
||||
let endorsers: Vec<T::AccountId> = (0..T::ParliamentaryEndorsements::get())
|
||||
.map(|i| account("endorser", i, 0))
|
||||
.collect();
|
||||
|
||||
// KYC check is already bypassed in test environment
|
||||
|
||||
Pallet::<T>::register_candidate(
|
||||
RawOrigin::Signed(candidate.clone()).into(),
|
||||
0,
|
||||
None,
|
||||
endorsers,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let election = ActiveElections::<T>::get(0).unwrap();
|
||||
pezframe_system::Pallet::<T>::set_block_number(election.voting_start);
|
||||
Pallet::<T>::cast_vote(RawOrigin::Signed(voter.clone()).into(), 0, vec![candidate], None)
|
||||
.unwrap();
|
||||
|
||||
// 2. Advance to election end time
|
||||
pezframe_system::Pallet::<T>::set_block_number(election.end_block + 1u32.into());
|
||||
|
||||
#[extrinsic_call]
|
||||
finalize_election(RawOrigin::Root, 0);
|
||||
|
||||
assert!(ElectionResults::<T>::get(0).is_some());
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// APPOINTMENT SYSTEM BENCHMARKS
|
||||
// ----------------------------------------------------------------
|
||||
#[benchmark]
|
||||
fn nominate_official() {
|
||||
// --- SETUP ---
|
||||
let nominator: T::AccountId = whitelisted_caller();
|
||||
let nominee: T::AccountId = account("nominee", 2, 0);
|
||||
let justification = b"Test nomination".to_vec().try_into().unwrap();
|
||||
|
||||
// Set nominator as Serok to pass authorization check
|
||||
CurrentOfficials::<T>::insert(GovernmentPosition::Serok, nominator.clone());
|
||||
|
||||
// Ensure the role is not already filled (clean state for benchmark)
|
||||
// AppointedOfficials storage should be empty for Dadger role
|
||||
// This is important because we added RoleAlreadyFilled check in lib.rs
|
||||
|
||||
#[extrinsic_call]
|
||||
nominate_official(
|
||||
RawOrigin::Signed(nominator),
|
||||
nominee,
|
||||
OfficialRole::Dadger,
|
||||
justification,
|
||||
);
|
||||
|
||||
assert_eq!(NextAppointmentId::<T>::get(), 1);
|
||||
// Verify that the role is still not filled (nomination doesn't fill it, approval does)
|
||||
assert!(!AppointedOfficials::<T>::contains_key(&OfficialRole::Dadger));
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn approve_appointment() {
|
||||
// --- SETUP ---
|
||||
let approver: T::AccountId = whitelisted_caller();
|
||||
let nominator: T::AccountId = account("nominator", 2, 0);
|
||||
let nominee: T::AccountId = account("nominee", 3, 0);
|
||||
let justification = b"Test nomination".to_vec().try_into().unwrap();
|
||||
|
||||
// Set nominator as Serok to pass authorization check for nomination
|
||||
CurrentOfficials::<T>::insert(GovernmentPosition::Serok, nominator.clone());
|
||||
|
||||
// Use a different role (Dozger) to avoid conflicts with nominate_official benchmark
|
||||
Pallet::<T>::nominate_official(
|
||||
RawOrigin::Signed(nominator).into(),
|
||||
nominee.clone(),
|
||||
OfficialRole::Dozger,
|
||||
justification,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Set approver as Serok to pass authorization check for approval
|
||||
CurrentOfficials::<T>::insert(GovernmentPosition::Serok, approver.clone());
|
||||
|
||||
#[extrinsic_call]
|
||||
approve_appointment(RawOrigin::Signed(approver), 0);
|
||||
|
||||
// Verify appointment ID incremented
|
||||
assert_eq!(NextAppointmentId::<T>::get(), 1);
|
||||
// CRITICAL: Verify that the role was assigned in AppointedOfficials storage
|
||||
// This tests the new storage write we added in lib.rs approve_appointment()
|
||||
assert_eq!(AppointedOfficials::<T>::get(&OfficialRole::Dozger), Some(nominee));
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// COLLECTIVE DECISION BENCHMARKS
|
||||
// ----------------------------------------------------------------
|
||||
#[benchmark]
|
||||
fn submit_proposal() {
|
||||
// --- SETUP ---
|
||||
let proposer: T::AccountId = whitelisted_caller();
|
||||
|
||||
// Simple member creation for benchmark
|
||||
let member: ParliamentMember<T> = ParliamentMember {
|
||||
account: proposer.clone(),
|
||||
elected_at: 0u32.into(),
|
||||
term_ends_at: 1000u32.into(),
|
||||
votes_participated: 0,
|
||||
total_votes_eligible: 0,
|
||||
participation_rate: 100,
|
||||
committees: Default::default(),
|
||||
};
|
||||
let members: BoundedVec<ParliamentMember<T>, T::ParliamentSize> =
|
||||
vec![member].try_into().unwrap();
|
||||
ParliamentMembers::<T>::put(members);
|
||||
|
||||
let title = b"Test Proposal".to_vec().try_into().unwrap();
|
||||
let description = b"Test proposal description".to_vec().try_into().unwrap();
|
||||
|
||||
#[extrinsic_call]
|
||||
submit_proposal(
|
||||
RawOrigin::Signed(proposer),
|
||||
title,
|
||||
description,
|
||||
CollectiveDecisionType::ParliamentSimpleMajority,
|
||||
ProposalPriority::Normal,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(ActiveProposals::<T>::get(0).is_some());
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn vote_on_proposal() {
|
||||
// --- SETUP ---
|
||||
let proposer: T::AccountId = account("proposer", 1, 0);
|
||||
let voter: T::AccountId = whitelisted_caller();
|
||||
|
||||
// Create two members (proposer and voter)
|
||||
let member1: ParliamentMember<T> = ParliamentMember {
|
||||
account: proposer.clone(),
|
||||
elected_at: 0u32.into(),
|
||||
term_ends_at: 1000u32.into(),
|
||||
votes_participated: 0,
|
||||
total_votes_eligible: 0,
|
||||
participation_rate: 100,
|
||||
committees: Default::default(),
|
||||
};
|
||||
let member2: ParliamentMember<T> = ParliamentMember {
|
||||
account: voter.clone(),
|
||||
elected_at: 0u32.into(),
|
||||
term_ends_at: 1000u32.into(),
|
||||
votes_participated: 0,
|
||||
total_votes_eligible: 0,
|
||||
participation_rate: 100,
|
||||
committees: Default::default(),
|
||||
};
|
||||
let members: BoundedVec<ParliamentMember<T>, T::ParliamentSize> =
|
||||
vec![member1, member2].try_into().unwrap();
|
||||
ParliamentMembers::<T>::put(members);
|
||||
|
||||
let title = b"Test Proposal".to_vec().try_into().unwrap();
|
||||
let description = b"Test proposal description".to_vec().try_into().unwrap();
|
||||
Pallet::<T>::submit_proposal(
|
||||
RawOrigin::Signed(proposer).into(),
|
||||
title,
|
||||
description,
|
||||
CollectiveDecisionType::ParliamentSimpleMajority,
|
||||
ProposalPriority::Normal,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let proposal = ActiveProposals::<T>::get(0).unwrap();
|
||||
pezframe_system::Pallet::<T>::set_block_number(proposal.voting_starts_at + 1u32.into());
|
||||
|
||||
let rationale = Some(b"Test vote rationale".to_vec().try_into().unwrap());
|
||||
|
||||
// Ensure voter hasn't voted yet (clean state for benchmark)
|
||||
// This tests our new ProposalAlreadyVoted check
|
||||
assert!(!CollectiveVotes::<T>::contains_key(0, &voter));
|
||||
|
||||
#[extrinsic_call]
|
||||
vote_on_proposal(RawOrigin::Signed(voter.clone()), 0, VoteChoice::Aye, rationale);
|
||||
|
||||
// Verify vote was recorded
|
||||
assert!(CollectiveVotes::<T>::get(0, &voter).is_some());
|
||||
// Verify the vote details are correct
|
||||
let vote = CollectiveVotes::<T>::get(0, &voter).unwrap();
|
||||
assert_eq!(vote.vote, VoteChoice::Aye);
|
||||
// This benchmark successfully tests:
|
||||
// 1. NotAuthorizedToVote check (voter is in ParliamentMembers)
|
||||
// 2. ProposalAlreadyVoted check (voter hasn't voted before)
|
||||
}
|
||||
|
||||
impl_benchmark_test_suite!(
|
||||
Pallet,
|
||||
crate::mock::ExtBuilder::default().build(),
|
||||
crate::mock::Test
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,385 @@
|
||||
//! Storage migrations for pezpallet-welati (Governance)
|
||||
|
||||
use super::*;
|
||||
use pezframe_support::{
|
||||
traits::{Get, GetStorageVersion, OnRuntimeUpgrade, StorageVersion},
|
||||
weights::Weight,
|
||||
};
|
||||
use pezsp_std::marker::PhantomData;
|
||||
|
||||
/// Current storage version
|
||||
pub const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
|
||||
|
||||
/// Migration from v0 to v1
|
||||
/// This migration handles the initial version setup for pezpallet-welati
|
||||
pub mod v1 {
|
||||
use super::*;
|
||||
|
||||
pub struct MigrateToV1<T>(PhantomData<T>);
|
||||
|
||||
impl<T: Config> OnRuntimeUpgrade for MigrateToV1<T> {
|
||||
fn on_runtime_upgrade() -> Weight {
|
||||
let current = Pallet::<T>::on_chain_storage_version();
|
||||
|
||||
log::info!(
|
||||
"🔄 Running migration for pezpallet-welati from {:?} to {:?}",
|
||||
current,
|
||||
STORAGE_VERSION
|
||||
);
|
||||
|
||||
if current == StorageVersion::new(0) {
|
||||
let migrated;
|
||||
let mut weight = Weight::zero();
|
||||
|
||||
// Example migration logic for governance storage
|
||||
// If storage format changes in the future, implement transformation here
|
||||
|
||||
// Count existing storage items for logging
|
||||
let officials_count = CurrentOfficials::<T>::iter().count() as u64;
|
||||
let ministers_count = CurrentMinisters::<T>::iter().count() as u64;
|
||||
let elections_count = ActiveElections::<T>::iter().count() as u64;
|
||||
let proposals_count = ActiveProposals::<T>::iter().count() as u64;
|
||||
|
||||
migrated = officials_count + ministers_count + elections_count + proposals_count;
|
||||
|
||||
// Update storage version
|
||||
STORAGE_VERSION.put::<Pallet<T>>();
|
||||
|
||||
log::info!("✅ Migrated {} entries in pezpallet-welati", migrated);
|
||||
log::info!(
|
||||
" Officials: {}, Ministers: {}, Elections: {}, Proposals: {}",
|
||||
officials_count,
|
||||
ministers_count,
|
||||
elections_count,
|
||||
proposals_count
|
||||
);
|
||||
|
||||
// Return weight used
|
||||
// Reads: all storage items + version read
|
||||
// Writes: version write
|
||||
weight = weight.saturating_add(T::DbWeight::get().reads_writes(migrated + 1, 1));
|
||||
|
||||
weight
|
||||
} else {
|
||||
log::info!(
|
||||
"👌 pezpallet-welati migration not needed, current version is {:?}",
|
||||
current
|
||||
);
|
||||
T::DbWeight::get().reads(1)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn pre_upgrade() -> Result<pezsp_std::vec::Vec<u8>, pezsp_runtime::TryRuntimeError> {
|
||||
let current = Pallet::<T>::on_chain_storage_version();
|
||||
|
||||
log::info!("🔍 Pre-upgrade check for pezpallet-welati");
|
||||
log::info!(" Current version: {:?}", current);
|
||||
|
||||
// Encode current storage counts for verification
|
||||
let officials_count = CurrentOfficials::<T>::iter().count() as u32;
|
||||
let ministers_count = CurrentMinisters::<T>::iter().count() as u32;
|
||||
let parliament_count = ParliamentMembers::<T>::get().len() as u32;
|
||||
let diwan_count = DiwanMembers::<T>::get().len() as u32;
|
||||
let appointed_count = AppointedOfficials::<T>::iter().count() as u32;
|
||||
let elections_count = ActiveElections::<T>::iter().count() as u32;
|
||||
let candidates_count = ElectionCandidates::<T>::iter().count() as u32;
|
||||
let votes_count = ElectionVotes::<T>::iter().count() as u32;
|
||||
let results_count = ElectionResults::<T>::iter().count() as u32;
|
||||
let districts_count = ElectoralDistrictConfig::<T>::iter().count() as u32;
|
||||
let nominations_count = PendingNominations::<T>::iter().count() as u32;
|
||||
let appointments_count = AppointmentProcesses::<T>::iter().count() as u32;
|
||||
let proposals_count = ActiveProposals::<T>::iter().count() as u32;
|
||||
let collective_votes_count = CollectiveVotes::<T>::iter().count() as u32;
|
||||
|
||||
log::info!(" CurrentOfficials entries: {}", officials_count);
|
||||
log::info!(" CurrentMinisters entries: {}", ministers_count);
|
||||
log::info!(" ParliamentMembers entries: {}", parliament_count);
|
||||
log::info!(" DiwanMembers entries: {}", diwan_count);
|
||||
log::info!(" AppointedOfficials entries: {}", appointed_count);
|
||||
log::info!(" ActiveElections entries: {}", elections_count);
|
||||
log::info!(" ElectionCandidates entries: {}", candidates_count);
|
||||
log::info!(" ElectionVotes entries: {}", votes_count);
|
||||
log::info!(" ElectionResults entries: {}", results_count);
|
||||
log::info!(" ElectoralDistrictConfig entries: {}", districts_count);
|
||||
log::info!(" PendingNominations entries: {}", nominations_count);
|
||||
log::info!(" AppointmentProcesses entries: {}", appointments_count);
|
||||
log::info!(" ActiveProposals entries: {}", proposals_count);
|
||||
log::info!(" CollectiveVotes entries: {}", collective_votes_count);
|
||||
|
||||
Ok((
|
||||
officials_count,
|
||||
ministers_count,
|
||||
parliament_count,
|
||||
diwan_count,
|
||||
appointed_count,
|
||||
elections_count,
|
||||
candidates_count,
|
||||
votes_count,
|
||||
results_count,
|
||||
districts_count,
|
||||
nominations_count,
|
||||
appointments_count,
|
||||
proposals_count,
|
||||
collective_votes_count,
|
||||
)
|
||||
.encode())
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn post_upgrade(state: pezsp_std::vec::Vec<u8>) -> Result<(), pezsp_runtime::TryRuntimeError> {
|
||||
use codec::Decode;
|
||||
|
||||
let (
|
||||
pre_officials_count,
|
||||
pre_ministers_count,
|
||||
pre_parliament_count,
|
||||
pre_diwan_count,
|
||||
pre_appointed_count,
|
||||
pre_elections_count,
|
||||
pre_candidates_count,
|
||||
pre_votes_count,
|
||||
pre_results_count,
|
||||
pre_districts_count,
|
||||
pre_nominations_count,
|
||||
pre_appointments_count,
|
||||
pre_proposals_count,
|
||||
pre_collective_votes_count,
|
||||
): (u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32) =
|
||||
Decode::decode(&mut &state[..])
|
||||
.map_err(|_| "Failed to decode pre-upgrade state")?;
|
||||
|
||||
log::info!("🔍 Post-upgrade check for pezpallet-welati");
|
||||
|
||||
// Verify storage version was updated
|
||||
let current_version = Pallet::<T>::on_chain_storage_version();
|
||||
assert_eq!(current_version, STORAGE_VERSION, "Storage version not updated correctly");
|
||||
log::info!("✅ Storage version updated to {:?}", current_version);
|
||||
|
||||
// Verify storage counts (should be same or more, never less)
|
||||
let post_officials_count = CurrentOfficials::<T>::iter().count() as u32;
|
||||
let post_ministers_count = CurrentMinisters::<T>::iter().count() as u32;
|
||||
let post_parliament_count = ParliamentMembers::<T>::get().len() as u32;
|
||||
let post_diwan_count = DiwanMembers::<T>::get().len() as u32;
|
||||
let post_appointed_count = AppointedOfficials::<T>::iter().count() as u32;
|
||||
let post_elections_count = ActiveElections::<T>::iter().count() as u32;
|
||||
let post_candidates_count = ElectionCandidates::<T>::iter().count() as u32;
|
||||
let post_votes_count = ElectionVotes::<T>::iter().count() as u32;
|
||||
let post_results_count = ElectionResults::<T>::iter().count() as u32;
|
||||
let post_districts_count = ElectoralDistrictConfig::<T>::iter().count() as u32;
|
||||
let post_nominations_count = PendingNominations::<T>::iter().count() as u32;
|
||||
let post_appointments_count = AppointmentProcesses::<T>::iter().count() as u32;
|
||||
let post_proposals_count = ActiveProposals::<T>::iter().count() as u32;
|
||||
let post_collective_votes_count = CollectiveVotes::<T>::iter().count() as u32;
|
||||
|
||||
log::info!(
|
||||
" CurrentOfficials entries: {} -> {}",
|
||||
pre_officials_count,
|
||||
post_officials_count
|
||||
);
|
||||
log::info!(
|
||||
" CurrentMinisters entries: {} -> {}",
|
||||
pre_ministers_count,
|
||||
post_ministers_count
|
||||
);
|
||||
log::info!(
|
||||
" ParliamentMembers entries: {} -> {}",
|
||||
pre_parliament_count,
|
||||
post_parliament_count
|
||||
);
|
||||
log::info!(" DiwanMembers entries: {} -> {}", pre_diwan_count, post_diwan_count);
|
||||
log::info!(
|
||||
" AppointedOfficials entries: {} -> {}",
|
||||
pre_appointed_count,
|
||||
post_appointed_count
|
||||
);
|
||||
log::info!(
|
||||
" ActiveElections entries: {} -> {}",
|
||||
pre_elections_count,
|
||||
post_elections_count
|
||||
);
|
||||
log::info!(
|
||||
" ElectionCandidates entries: {} -> {}",
|
||||
pre_candidates_count,
|
||||
post_candidates_count
|
||||
);
|
||||
log::info!(" ElectionVotes entries: {} -> {}", pre_votes_count, post_votes_count);
|
||||
log::info!(
|
||||
" ElectionResults entries: {} -> {}",
|
||||
pre_results_count,
|
||||
post_results_count
|
||||
);
|
||||
log::info!(
|
||||
" ElectoralDistrictConfig entries: {} -> {}",
|
||||
pre_districts_count,
|
||||
post_districts_count
|
||||
);
|
||||
log::info!(
|
||||
" PendingNominations entries: {} -> {}",
|
||||
pre_nominations_count,
|
||||
post_nominations_count
|
||||
);
|
||||
log::info!(
|
||||
" AppointmentProcesses entries: {} -> {}",
|
||||
pre_appointments_count,
|
||||
post_appointments_count
|
||||
);
|
||||
log::info!(
|
||||
" ActiveProposals entries: {} -> {}",
|
||||
pre_proposals_count,
|
||||
post_proposals_count
|
||||
);
|
||||
log::info!(
|
||||
" CollectiveVotes entries: {} -> {}",
|
||||
pre_collective_votes_count,
|
||||
post_collective_votes_count
|
||||
);
|
||||
|
||||
// Verify no data was lost
|
||||
assert!(
|
||||
post_officials_count >= pre_officials_count,
|
||||
"CurrentOfficials entries decreased during migration"
|
||||
);
|
||||
assert!(
|
||||
post_ministers_count >= pre_ministers_count,
|
||||
"CurrentMinisters entries decreased during migration"
|
||||
);
|
||||
assert!(
|
||||
post_parliament_count >= pre_parliament_count,
|
||||
"ParliamentMembers entries decreased during migration"
|
||||
);
|
||||
assert!(
|
||||
post_diwan_count >= pre_diwan_count,
|
||||
"DiwanMembers entries decreased during migration"
|
||||
);
|
||||
assert!(
|
||||
post_appointed_count >= pre_appointed_count,
|
||||
"AppointedOfficials entries decreased during migration"
|
||||
);
|
||||
assert!(
|
||||
post_elections_count >= pre_elections_count,
|
||||
"ActiveElections entries decreased during migration"
|
||||
);
|
||||
assert!(
|
||||
post_candidates_count >= pre_candidates_count,
|
||||
"ElectionCandidates entries decreased during migration"
|
||||
);
|
||||
assert!(
|
||||
post_votes_count >= pre_votes_count,
|
||||
"ElectionVotes entries decreased during migration"
|
||||
);
|
||||
assert!(
|
||||
post_results_count >= pre_results_count,
|
||||
"ElectionResults entries decreased during migration"
|
||||
);
|
||||
assert!(
|
||||
post_districts_count >= pre_districts_count,
|
||||
"ElectoralDistrictConfig entries decreased during migration"
|
||||
);
|
||||
assert!(
|
||||
post_nominations_count >= pre_nominations_count,
|
||||
"PendingNominations entries decreased during migration"
|
||||
);
|
||||
assert!(
|
||||
post_appointments_count >= pre_appointments_count,
|
||||
"AppointmentProcesses entries decreased during migration"
|
||||
);
|
||||
assert!(
|
||||
post_proposals_count >= pre_proposals_count,
|
||||
"ActiveProposals entries decreased during migration"
|
||||
);
|
||||
assert!(
|
||||
post_collective_votes_count >= pre_collective_votes_count,
|
||||
"CollectiveVotes entries decreased during migration"
|
||||
);
|
||||
|
||||
log::info!("✅ Post-upgrade checks passed for pezpallet-welati");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Example migration for future version changes
|
||||
/// This demonstrates how to handle storage format changes in governance data
|
||||
pub mod v2 {
|
||||
use super::*;
|
||||
|
||||
/// Example: Migration when election or proposal format changes
|
||||
pub struct MigrateToV2<T>(PhantomData<T>);
|
||||
|
||||
impl<T: Config> OnRuntimeUpgrade for MigrateToV2<T> {
|
||||
fn on_runtime_upgrade() -> Weight {
|
||||
let current = Pallet::<T>::on_chain_storage_version();
|
||||
|
||||
if current < StorageVersion::new(2) {
|
||||
log::info!("🔄 Running migration for pezpallet-welati to v2");
|
||||
|
||||
// Example migration logic
|
||||
// 1. Transform election data if format changed
|
||||
// 2. Migrate proposal structure if needed
|
||||
// 3. Update parliament/diwan member format
|
||||
// 4. Update version
|
||||
|
||||
// For now, this is just a template
|
||||
StorageVersion::new(2).put::<Pallet<T>>();
|
||||
|
||||
log::info!("✅ Completed migration to pezpallet-welati v2");
|
||||
|
||||
T::DbWeight::get().reads_writes(1, 1)
|
||||
} else {
|
||||
log::info!("👌 pezpallet-welati v2 migration not needed");
|
||||
T::DbWeight::get().reads(1)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn pre_upgrade() -> Result<pezsp_std::vec::Vec<u8>, pezsp_runtime::TryRuntimeError> {
|
||||
log::info!("🔍 Pre-upgrade check for pezpallet-welati v2");
|
||||
Ok(pezsp_std::vec::Vec::new())
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn post_upgrade(_state: pezsp_std::vec::Vec<u8>) -> Result<(), pezsp_runtime::TryRuntimeError> {
|
||||
log::info!("✅ Post-upgrade check passed for pezpallet-welati v2");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::mock::{ExtBuilder, Test};
|
||||
use pezframe_support::traits::OnRuntimeUpgrade;
|
||||
|
||||
#[test]
|
||||
fn test_migration_v1() {
|
||||
ExtBuilder::default().build().execute_with(|| {
|
||||
// Set initial storage version to 0
|
||||
StorageVersion::new(0).put::<Pallet<Test>>();
|
||||
|
||||
// Run migration
|
||||
let weight = v1::MigrateToV1::<Test>::on_runtime_upgrade();
|
||||
|
||||
// Verify version was updated
|
||||
assert_eq!(Pallet::<Test>::on_chain_storage_version(), STORAGE_VERSION);
|
||||
|
||||
// Verify weight is non-zero
|
||||
assert!(weight != Weight::zero());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_migration_idempotent() {
|
||||
ExtBuilder::default().build().execute_with(|| {
|
||||
// Set current version
|
||||
STORAGE_VERSION.put::<Pallet<Test>>();
|
||||
|
||||
// Run migration again
|
||||
let weight = v1::MigrateToV1::<Test>::on_runtime_upgrade();
|
||||
|
||||
// Should be a no-op
|
||||
assert_eq!(weight, pezframe_support::weights::constants::RocksDbWeight::get().reads(1));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,483 @@
|
||||
use crate::{self as pezpallet_welati, *};
|
||||
use pezframe_support::{
|
||||
assert_ok, construct_runtime, derive_impl, parameter_types,
|
||||
traits::{AsEnsureOriginWithArg, ConstU128, ConstU32, ConstU64, Everything, Randomness},
|
||||
BoundedVec,
|
||||
};
|
||||
use pezsp_core::H256;
|
||||
use pezsp_runtime::{
|
||||
traits::{BlakeTwo256, IdentityLookup},
|
||||
BuildStorage,
|
||||
};
|
||||
|
||||
type Block = pezframe_system::mocking::MockBlock<Test>;
|
||||
type AccountId = u64;
|
||||
type Balance = u128;
|
||||
|
||||
// Runtime with pezpallet-identity included for pezpallet-tiki dependency
|
||||
construct_runtime!(
|
||||
pub enum Test {
|
||||
System: pezframe_system,
|
||||
Balances: pezpallet_balances,
|
||||
Timestamp: pezpallet_timestamp,
|
||||
Nfts: pezpallet_nfts,
|
||||
Identity: pezpallet_identity,
|
||||
IdentityKyc: pezpallet_identity_kyc,
|
||||
Tiki: pezpallet_tiki,
|
||||
Trust: pezpallet_trust,
|
||||
StakingScore: pezpallet_staking_score,
|
||||
Referral: pezpallet_referral,
|
||||
Welati: pezpallet_welati,
|
||||
}
|
||||
);
|
||||
|
||||
parameter_types! {
|
||||
pub const BlockHashCount: u64 = 250;
|
||||
pub const SS58Prefix: u8 = 42;
|
||||
}
|
||||
|
||||
#[derive_impl(pezframe_system::config_preludes::TestDefaultConfig as pezframe_system::DefaultConfig)]
|
||||
impl pezframe_system::Config for Test {
|
||||
type BaseCallFilter = Everything;
|
||||
type BlockWeights = ();
|
||||
type BlockLength = ();
|
||||
type DbWeight = pezframe_support::weights::constants::RocksDbWeight;
|
||||
type RuntimeOrigin = RuntimeOrigin;
|
||||
type RuntimeCall = RuntimeCall;
|
||||
type Nonce = u64;
|
||||
type Hash = H256;
|
||||
type Hashing = BlakeTwo256;
|
||||
type AccountId = AccountId;
|
||||
type Lookup = IdentityLookup<Self::AccountId>;
|
||||
type Block = Block;
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type BlockHashCount = BlockHashCount;
|
||||
type Version = ();
|
||||
type PalletInfo = PalletInfo;
|
||||
type AccountData = pezpallet_balances::AccountData<Balance>;
|
||||
type OnNewAccount = ();
|
||||
type OnKilledAccount = ();
|
||||
type SystemWeightInfo = ();
|
||||
type SS58Prefix = SS58Prefix;
|
||||
type OnSetCode = ();
|
||||
type MaxConsumers = ConstU32<16>;
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const ExistentialDeposit: Balance = 1;
|
||||
pub const MaxLocks: u32 = 50;
|
||||
pub const MaxReserves: u32 = 50;
|
||||
}
|
||||
|
||||
impl pezpallet_balances::Config for Test {
|
||||
type MaxLocks = MaxLocks;
|
||||
type MaxReserves = MaxReserves;
|
||||
type ReserveIdentifier = [u8; 8];
|
||||
type Balance = Balance;
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type DustRemoval = ();
|
||||
type ExistentialDeposit = ExistentialDeposit;
|
||||
type AccountStore = System;
|
||||
type WeightInfo = ();
|
||||
type FreezeIdentifier = ();
|
||||
type MaxFreezes = ConstU32<0>;
|
||||
type RuntimeHoldReason = ();
|
||||
type RuntimeFreezeReason = ();
|
||||
type DoneSlashHandler = ();
|
||||
}
|
||||
|
||||
impl pezpallet_timestamp::Config for Test {
|
||||
type Moment = u64;
|
||||
type OnTimestampSet = ();
|
||||
type MinimumPeriod = ConstU64<1>;
|
||||
type WeightInfo = ();
|
||||
}
|
||||
|
||||
// Mock Randomness - SADECE BİR KEZ TANIMLA
|
||||
pub struct MockRandomness;
|
||||
impl Randomness<H256, u64> for MockRandomness {
|
||||
fn random(_subject: &[u8]) -> (H256, u64) {
|
||||
(H256::default(), 0)
|
||||
}
|
||||
}
|
||||
|
||||
// NFTs Configuration
|
||||
parameter_types! {
|
||||
pub const CollectionDeposit: Balance = 0;
|
||||
pub const ItemDeposit: Balance = 0;
|
||||
pub const StringLimit: u32 = 64;
|
||||
pub const KeyLimit: u32 = 32;
|
||||
pub const ValueLimit: u32 = 64;
|
||||
pub const ApprovalsLimit: u32 = 1;
|
||||
pub const ItemAttributesApprovalsLimit: u32 = 1;
|
||||
pub const MaxTips: u32 = 1;
|
||||
pub const MaxDeadlineDuration: u64 = 1000;
|
||||
pub const MaxAttributesPerCall: u32 = 1;
|
||||
}
|
||||
|
||||
impl pezpallet_nfts::Config for Test {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type CollectionId = u32;
|
||||
type ItemId = u32;
|
||||
type Currency = Balances;
|
||||
type CreateOrigin = AsEnsureOriginWithArg<pezframe_system::EnsureSigned<AccountId>>;
|
||||
type ForceOrigin = pezframe_system::EnsureRoot<AccountId>;
|
||||
type Locker = ();
|
||||
type CollectionDeposit = CollectionDeposit;
|
||||
type ItemDeposit = ItemDeposit;
|
||||
type MetadataDepositBase = ConstU128<0>;
|
||||
type AttributeDepositBase = ConstU128<0>;
|
||||
type DepositPerByte = ConstU128<0>;
|
||||
type StringLimit = StringLimit;
|
||||
type KeyLimit = KeyLimit;
|
||||
type ValueLimit = ValueLimit;
|
||||
type ApprovalsLimit = ApprovalsLimit;
|
||||
type ItemAttributesApprovalsLimit = ItemAttributesApprovalsLimit;
|
||||
type MaxTips = MaxTips;
|
||||
type MaxDeadlineDuration = MaxDeadlineDuration;
|
||||
type MaxAttributesPerCall = MaxAttributesPerCall;
|
||||
type Features = ();
|
||||
type OffchainSignature = pezsp_runtime::testing::TestSignature;
|
||||
type OffchainPublic = pezsp_runtime::testing::UintAuthorityId;
|
||||
type WeightInfo = ();
|
||||
type BlockNumberProvider = System;
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
type Helper = ();
|
||||
}
|
||||
|
||||
// Identity Configuration - MINIMAL for pezpallet-tiki dependency
|
||||
parameter_types! {
|
||||
pub const BasicDeposit: Balance = 10;
|
||||
pub const ByteDeposit: Balance = 1;
|
||||
pub const SubAccountDeposit: Balance = 10;
|
||||
pub const MaxSubAccounts: u32 = 2;
|
||||
pub const MaxRegistrars: u32 = 2;
|
||||
pub const MaxAdditionalFields: u32 = 2;
|
||||
pub const UsernameDeposit: Balance = 100;
|
||||
pub const MaxUsernameLength: u32 = 32;
|
||||
pub const MaxSuffixLength: u32 = 7;
|
||||
pub const PendingUsernameExpiration: u64 = 100;
|
||||
pub const UsernameGracePeriod: u64 = 100;
|
||||
}
|
||||
|
||||
impl pezpallet_identity::Config for Test {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type Currency = Balances;
|
||||
type Slashed = ();
|
||||
type ForceOrigin = pezframe_system::EnsureRoot<AccountId>;
|
||||
type RegistrarOrigin = pezframe_system::EnsureRoot<AccountId>;
|
||||
type WeightInfo = ();
|
||||
type BasicDeposit = BasicDeposit;
|
||||
type SubAccountDeposit = SubAccountDeposit;
|
||||
type MaxSubAccounts = MaxSubAccounts;
|
||||
type MaxRegistrars = MaxRegistrars;
|
||||
type IdentityInformation = pezpallet_identity::legacy::IdentityInfo<MaxAdditionalFields>;
|
||||
type ByteDeposit = ByteDeposit;
|
||||
type UsernameDeposit = UsernameDeposit;
|
||||
type MaxUsernameLength = MaxUsernameLength;
|
||||
type MaxSuffixLength = MaxSuffixLength;
|
||||
type PendingUsernameExpiration = PendingUsernameExpiration;
|
||||
type UsernameGracePeriod = UsernameGracePeriod;
|
||||
type UsernameAuthorityOrigin = pezframe_system::EnsureRoot<AccountId>;
|
||||
type OffchainSignature = pezsp_runtime::testing::TestSignature;
|
||||
type SigningPublicKey = pezsp_runtime::testing::UintAuthorityId;
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
type BenchmarkHelper = ();
|
||||
}
|
||||
|
||||
// Identity KYC Configuration
|
||||
parameter_types! {
|
||||
pub const KycApplicationDeposit: Balance = 1_000;
|
||||
pub const MaxStringLength: u32 = 128;
|
||||
pub const MaxCidLength: u32 = 64;
|
||||
}
|
||||
|
||||
pub struct NoOpOnKycApproved;
|
||||
impl pezpallet_identity_kyc::types::OnKycApproved<AccountId> for NoOpOnKycApproved {
|
||||
fn on_kyc_approved(_who: &AccountId, _referrer: &AccountId) {}
|
||||
}
|
||||
|
||||
pub struct NoOpOnCitizenshipRevoked;
|
||||
impl pezpallet_identity_kyc::types::OnCitizenshipRevoked<AccountId> for NoOpOnCitizenshipRevoked {
|
||||
fn on_citizenship_revoked(_who: &AccountId) {}
|
||||
}
|
||||
|
||||
pub struct NoOpCitizenNftProvider;
|
||||
impl pezpallet_identity_kyc::types::CitizenNftProvider<AccountId> for NoOpCitizenNftProvider {
|
||||
fn mint_citizen_nft(_who: &AccountId) -> Result<(), pezsp_runtime::DispatchError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn mint_citizen_nft_confirmed(_who: &AccountId) -> Result<(), pezsp_runtime::DispatchError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn burn_citizen_nft(_who: &AccountId) -> Result<(), pezsp_runtime::DispatchError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl pezpallet_identity_kyc::Config for Test {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type Currency = Balances;
|
||||
type GovernanceOrigin = pezframe_system::EnsureRoot<AccountId>;
|
||||
type WeightInfo = ();
|
||||
type OnKycApproved = NoOpOnKycApproved;
|
||||
type OnCitizenshipRevoked = NoOpOnCitizenshipRevoked;
|
||||
type CitizenNftProvider = NoOpCitizenNftProvider;
|
||||
type KycApplicationDeposit = KycApplicationDeposit;
|
||||
type MaxStringLength = MaxStringLength;
|
||||
type MaxCidLength = MaxCidLength;
|
||||
}
|
||||
|
||||
// Mock StakingInfo provider - SADECE BİR KEZ TANIMLA
|
||||
pub struct MockStakingInfo;
|
||||
impl pezpallet_staking_score::StakingInfoProvider<AccountId, Balance> for MockStakingInfo {
|
||||
fn get_staking_details(
|
||||
_account: &AccountId,
|
||||
) -> Option<pezpallet_staking_score::StakingDetails<Balance>> {
|
||||
Some(pezpallet_staking_score::StakingDetails {
|
||||
staked_amount: 1000u128,
|
||||
nominations_count: 0,
|
||||
unlocking_chunks_count: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Staking Score Configuration
|
||||
impl pezpallet_staking_score::Config for Test {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type WeightInfo = ();
|
||||
type Balance = Balance;
|
||||
type StakingInfo = MockStakingInfo;
|
||||
}
|
||||
|
||||
// Referral Configuration
|
||||
parameter_types! {
|
||||
pub const DefaultReferrerAccount: AccountId = 1;
|
||||
pub const PenaltyPerRevocation: u32 = 10;
|
||||
}
|
||||
|
||||
impl pezpallet_referral::Config for Test {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type WeightInfo = ();
|
||||
type DefaultReferrer = DefaultReferrerAccount;
|
||||
type PenaltyPerRevocation = PenaltyPerRevocation;
|
||||
}
|
||||
|
||||
// Tiki Configuration
|
||||
parameter_types! {
|
||||
pub const MaxTikisPerUser: u32 = 50;
|
||||
pub const TikiCollectionId: u32 = 0;
|
||||
}
|
||||
|
||||
impl pezpallet_tiki::Config for Test {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type AdminOrigin = pezframe_system::EnsureRoot<AccountId>;
|
||||
type WeightInfo = ();
|
||||
type MaxTikisPerUser = MaxTikisPerUser;
|
||||
type Tiki = pezpallet_tiki::Tiki;
|
||||
type TikiCollectionId = TikiCollectionId;
|
||||
}
|
||||
|
||||
// Mock implementations for required traits - YÜKSEK SKORLAR VER
|
||||
pub struct MockStakingScoreProvider;
|
||||
impl pezpallet_staking_score::StakingScoreProvider<AccountId, u64> for MockStakingScoreProvider {
|
||||
fn get_staking_score(_account: &AccountId) -> (u32, u64) {
|
||||
(1000, 0) // Yüksek skor
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MockReferralScoreProvider;
|
||||
impl pezpallet_trust::ReferralScoreProvider<AccountId> for MockReferralScoreProvider {
|
||||
fn get_referral_score(_account: &AccountId) -> u32 {
|
||||
500 // Yüksek skor
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MockPerwerdeScoreProvider;
|
||||
impl pezpallet_trust::PerwerdeScoreProvider<AccountId> for MockPerwerdeScoreProvider {
|
||||
fn get_perwerde_score(_account: &AccountId) -> u32 {
|
||||
750 // Yüksek skor
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MockTikiScoreProvider;
|
||||
|
||||
// `pezpallet_trust` için implementasyon
|
||||
impl pezpallet_trust::TikiScoreProvider<AccountId> for MockTikiScoreProvider {
|
||||
fn get_tiki_score(_account: &AccountId) -> u32 {
|
||||
100
|
||||
}
|
||||
}
|
||||
|
||||
// `pezpallet_welati`'nin ihtiyaç duyduğu `pezpallet_tiki` için implementasyon
|
||||
impl pezpallet_tiki::TikiScoreProvider<AccountId> for MockTikiScoreProvider {
|
||||
fn get_tiki_score(_account: &AccountId) -> u32 {
|
||||
1000 // Yüksek Tiki score - tüm kontrolleri geçer
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MockCitizenshipStatusProvider;
|
||||
impl pezpallet_trust::CitizenshipStatusProvider<AccountId> for MockCitizenshipStatusProvider {
|
||||
fn is_citizen(_account: &AccountId) -> bool {
|
||||
true // Herkes vatandaş
|
||||
}
|
||||
}
|
||||
|
||||
// MOCK TRUST PROVIDER - HERKES İÇİN YÜKSEK SKOR
|
||||
pub struct MockTrustProvider;
|
||||
impl pezpallet_trust::TrustScoreProvider<AccountId> for MockTrustProvider {
|
||||
fn trust_score_of(_account: &AccountId) -> u128 {
|
||||
1000u128 // Herkes için yüksek trust score
|
||||
}
|
||||
}
|
||||
|
||||
// CitizenInfo trait implementation for MockTrustProvider
|
||||
impl CitizenInfo for MockTrustProvider {
|
||||
fn citizen_count() -> u32 {
|
||||
110
|
||||
}
|
||||
}
|
||||
|
||||
// Trust Configuration
|
||||
parameter_types! {
|
||||
pub const ScoreMultiplierBase: u128 = 100;
|
||||
pub const UpdateInterval: u64 = 1000;
|
||||
pub const MaxBatchSize: u32 = 100;
|
||||
}
|
||||
|
||||
impl pezpallet_trust::Config for Test {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type WeightInfo = ();
|
||||
type Score = u128;
|
||||
type ScoreMultiplierBase = ScoreMultiplierBase;
|
||||
type UpdateInterval = UpdateInterval;
|
||||
type MaxBatchSize = MaxBatchSize;
|
||||
type StakingScoreSource = MockStakingScoreProvider;
|
||||
type ReferralScoreSource = MockReferralScoreProvider;
|
||||
type PerwerdeScoreSource = MockPerwerdeScoreProvider;
|
||||
type TikiScoreSource = MockTikiScoreProvider;
|
||||
type CitizenshipSource = MockCitizenshipStatusProvider;
|
||||
}
|
||||
|
||||
// Welati Configuration - SADECE BİR KEZ TANIMLA
|
||||
parameter_types! {
|
||||
pub const ParliamentSize: u32 = 201;
|
||||
pub const DiwanSize: u32 = 11;
|
||||
pub const ElectionPeriod: u64 = 432_000;
|
||||
pub const CandidacyPeriod: u64 = 86_400;
|
||||
pub const CampaignPeriod: u64 = 259_200;
|
||||
pub const ElectoralDistricts: u32 = 10;
|
||||
pub const CandidacyDeposit: u128 = 10_000;
|
||||
pub const PresidentialEndorsements: u32 = 100;
|
||||
pub const ParliamentaryEndorsements: u32 = 50;
|
||||
}
|
||||
|
||||
impl pezpallet_welati::Config for Test {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type WeightInfo = ();
|
||||
type Randomness = MockRandomness;
|
||||
type RuntimeCall = RuntimeCall;
|
||||
type TrustScoreSource = MockTrustProvider; // Mock provider kullan
|
||||
type TikiSource = MockTikiScoreProvider; // Mock Tiki provider kullan
|
||||
type CitizenSource = MockTrustProvider; // Mock provider kullan
|
||||
type KycSource = IdentityKyc;
|
||||
type ParliamentSize = ParliamentSize;
|
||||
type DiwanSize = DiwanSize;
|
||||
type ElectionPeriod = ElectionPeriod;
|
||||
type CandidacyPeriod = CandidacyPeriod;
|
||||
type CampaignPeriod = CampaignPeriod;
|
||||
type ElectoralDistricts = ElectoralDistricts;
|
||||
type CandidacyDeposit = CandidacyDeposit;
|
||||
type PresidentialEndorsements = PresidentialEndorsements;
|
||||
type ParliamentaryEndorsements = ParliamentaryEndorsements;
|
||||
}
|
||||
|
||||
// CRITICAL: CitizenInfo trait implementation - SADECE BİR KEZ TANIMLA
|
||||
impl CitizenInfo for Trust {
|
||||
fn citizen_count() -> u32 {
|
||||
110
|
||||
}
|
||||
}
|
||||
|
||||
// Test externalities builder
|
||||
pub struct ExtBuilder {
|
||||
balances: Vec<(AccountId, Balance)>,
|
||||
}
|
||||
|
||||
impl Default for ExtBuilder {
|
||||
fn default() -> Self {
|
||||
Self { balances: (1..=110).map(|i| (i as AccountId, 100_000_000_000_000)).collect() }
|
||||
}
|
||||
}
|
||||
|
||||
impl ExtBuilder {
|
||||
pub fn build(self) -> pezsp_io::TestExternalities {
|
||||
let mut t = pezframe_system::GenesisConfig::<Test>::default().build_storage().unwrap();
|
||||
|
||||
pezpallet_balances::GenesisConfig::<Test> { balances: self.balances, dev_accounts: None }
|
||||
.assimilate_storage(&mut t)
|
||||
.unwrap();
|
||||
|
||||
let mut ext = pezsp_io::TestExternalities::new(t);
|
||||
ext.execute_with(|| {
|
||||
System::set_block_number(1);
|
||||
|
||||
assert_ok!(Nfts::create(RuntimeOrigin::signed(1), 1, Default::default()));
|
||||
|
||||
setup_test_users();
|
||||
});
|
||||
ext
|
||||
}
|
||||
}
|
||||
|
||||
// SIMPLIFIED TEST USER SETUP - BOŞ BIRAK, MOCK PROVIDERS YETERLI
|
||||
pub fn setup_test_users() {
|
||||
// Mock provider'lar zaten herkesin yüksek trust score'u olmasını sağlıyor
|
||||
// ve TikiScoreProvider da herkesin Tiki'ye sahip olduğunu söylüyor
|
||||
// Bu sayede pezpallet-tiki ile uğraşmak zorunda kalmıyoruz
|
||||
|
||||
// Sadece NFTs collection'ı oluşturuldu, bu yeterli
|
||||
// Testlerde KYC kontrolü zaten bypass ediliyor
|
||||
}
|
||||
|
||||
// CRITICAL HELPER FUNCTION FOR TESTS
|
||||
pub fn add_parliament_member(account: AccountId) {
|
||||
let member = ParliamentMember {
|
||||
account,
|
||||
elected_at: System::block_number(),
|
||||
term_ends_at: System::block_number() + 100_000,
|
||||
votes_participated: 0,
|
||||
total_votes_eligible: 0,
|
||||
participation_rate: 100,
|
||||
committees: BoundedVec::default(),
|
||||
};
|
||||
|
||||
let mut members = ParliamentMembers::<Test>::get();
|
||||
if members.try_push(member).is_ok() {
|
||||
ParliamentMembers::<Test>::put(members);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_to_block(n: u64) {
|
||||
while System::block_number() < n {
|
||||
if System::block_number() > 0 {
|
||||
System::on_finalize(System::block_number());
|
||||
Welati::on_finalize(System::block_number());
|
||||
}
|
||||
System::set_block_number(System::block_number() + 1);
|
||||
Welati::on_initialize(System::block_number());
|
||||
System::on_initialize(System::block_number());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn last_event() -> RuntimeEvent {
|
||||
System::events().pop().expect("Event expected").event
|
||||
}
|
||||
|
||||
pub fn events() -> Vec<RuntimeEvent> {
|
||||
let evt = System::events().into_iter().map(|evt| evt.event).collect::<Vec<_>>();
|
||||
System::reset_events();
|
||||
evt
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,335 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
|
||||
//! Autogenerated weights for `pezpallet_welati`
|
||||
//!
|
||||
//! THIS FILE WAS AUTO-GENERATED USING THE BIZINIKIWI BENCHMARK CLI VERSION 32.0.0
|
||||
//! DATE: 2025-12-08, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
|
||||
//! WORST CASE MAP SIZE: `1000000`
|
||||
//! HOSTNAME: `MamostePC`, CPU: `11th Gen Intel(R) Core(TM) i9-11950H @ 2.60GHz`
|
||||
//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: `1024`
|
||||
|
||||
// Executed Command:
|
||||
// ./target/release/frame-omni-bencher
|
||||
// v1
|
||||
// benchmark
|
||||
// pallet
|
||||
// --runtime
|
||||
// target/release/wbuild/people-pezkuwichain-runtime/people_pezkuwichain_runtime.compact.compressed.wasm
|
||||
// --pallets
|
||||
// pezpallet_welati
|
||||
// -e
|
||||
// all
|
||||
// --steps
|
||||
// 50
|
||||
// --repeat
|
||||
// 20
|
||||
// --output
|
||||
// pezcumulus/teyrchains/pallets/welati/src/weights.rs
|
||||
// --template
|
||||
// bizinikiwi/.maintain/frame-weight-template.hbs
|
||||
|
||||
#![cfg_attr(rustfmt, rustfmt_skip)]
|
||||
#![allow(unused_parens)]
|
||||
#![allow(unused_imports)]
|
||||
#![allow(missing_docs)]
|
||||
#![allow(dead_code)]
|
||||
|
||||
use pezframe_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}};
|
||||
use core::marker::PhantomData;
|
||||
|
||||
/// Weight functions needed for `pezpallet_welati`.
|
||||
pub trait WeightInfo {
|
||||
fn initiate_election() -> Weight;
|
||||
fn register_candidate() -> Weight;
|
||||
fn cast_vote() -> Weight;
|
||||
fn finalize_election() -> Weight;
|
||||
fn nominate_official() -> Weight;
|
||||
fn approve_appointment() -> Weight;
|
||||
fn submit_proposal() -> Weight;
|
||||
fn vote_on_proposal() -> Weight;
|
||||
}
|
||||
|
||||
/// Weights for `pezpallet_welati` using the Bizinikiwi node and recommended hardware.
|
||||
pub struct BizinikiwiWeight<T>(PhantomData<T>);
|
||||
impl<T: pezframe_system::Config> WeightInfo for BizinikiwiWeight<T> {
|
||||
/// Storage: `Welati::NextElectionId` (r:1 w:1)
|
||||
/// Proof: `Welati::NextElectionId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Welati::ActiveElections` (r:0 w:1)
|
||||
/// Proof: `Welati::ActiveElections` (`max_values`: None, `max_size`: Some(29354), added: 31829, mode: `MaxEncodedLen`)
|
||||
fn initiate_election() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `42`
|
||||
// Estimated: `1489`
|
||||
// Minimum execution time: 21_177_000 picoseconds.
|
||||
Weight::from_parts(22_712_000, 1489)
|
||||
.saturating_add(T::DbWeight::get().reads(1_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(2_u64))
|
||||
}
|
||||
/// Storage: `Welati::ActiveElections` (r:1 w:1)
|
||||
/// Proof: `Welati::ActiveElections` (`max_values`: None, `max_size`: Some(29354), added: 31829, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Welati::ElectionCandidates` (r:1 w:1)
|
||||
/// Proof: `Welati::ElectionCandidates` (`max_values`: None, `max_size`: Some(3833), added: 6308, mode: `MaxEncodedLen`)
|
||||
fn register_candidate() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `145`
|
||||
// Estimated: `32819`
|
||||
// Minimum execution time: 32_272_000 picoseconds.
|
||||
Weight::from_parts(39_628_000, 32819)
|
||||
.saturating_add(T::DbWeight::get().reads(2_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(2_u64))
|
||||
}
|
||||
/// Storage: `Welati::ActiveElections` (r:1 w:1)
|
||||
/// Proof: `Welati::ActiveElections` (`max_values`: None, `max_size`: Some(29354), added: 31829, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Welati::ElectionVotes` (r:1 w:1)
|
||||
/// Proof: `Welati::ElectionVotes` (`max_values`: None, `max_size`: Some(435), added: 2910, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Welati::ElectionCandidates` (r:1 w:1)
|
||||
/// Proof: `Welati::ElectionCandidates` (`max_values`: None, `max_size`: Some(3833), added: 6308, mode: `MaxEncodedLen`)
|
||||
fn cast_vote() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `3532`
|
||||
// Estimated: `32819`
|
||||
// Minimum execution time: 27_874_000 picoseconds.
|
||||
Weight::from_parts(29_000_000, 32819)
|
||||
.saturating_add(T::DbWeight::get().reads(3_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(3_u64))
|
||||
}
|
||||
/// Storage: `Welati::ActiveElections` (r:1 w:1)
|
||||
/// Proof: `Welati::ActiveElections` (`max_values`: None, `max_size`: Some(29354), added: 31829, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Welati::ElectionCandidates` (r:1 w:0)
|
||||
/// Proof: `Welati::ElectionCandidates` (`max_values`: None, `max_size`: Some(3833), added: 6308, mode: `MaxEncodedLen`)
|
||||
/// Storage: `IdentityKyc::KycStatuses` (r:2 w:0)
|
||||
/// Proof: `IdentityKyc::KycStatuses` (`max_values`: None, `max_size`: Some(49), added: 2524, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Welati::ElectionResults` (r:0 w:1)
|
||||
/// Proof: `Welati::ElectionResults` (`max_values`: None, `max_size`: Some(6467), added: 8942, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Welati::ParliamentMembers` (r:0 w:1)
|
||||
/// Proof: `Welati::ParliamentMembers` (`max_values`: Some(1), `max_size`: Some(11057), added: 11552, mode: `MaxEncodedLen`)
|
||||
fn finalize_election() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `3682`
|
||||
// Estimated: `32819`
|
||||
// Minimum execution time: 35_614_000 picoseconds.
|
||||
Weight::from_parts(38_634_000, 32819)
|
||||
.saturating_add(T::DbWeight::get().reads(4_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(3_u64))
|
||||
}
|
||||
/// Storage: `Welati::CurrentOfficials` (r:1 w:0)
|
||||
/// Proof: `Welati::CurrentOfficials` (`max_values`: None, `max_size`: Some(49), added: 2524, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Welati::CurrentMinisters` (r:1 w:0)
|
||||
/// Proof: `Welati::CurrentMinisters` (`max_values`: None, `max_size`: Some(49), added: 2524, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Welati::AppointedOfficials` (r:1 w:0)
|
||||
/// Proof: `Welati::AppointedOfficials` (`max_values`: None, `max_size`: Some(49), added: 2524, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Welati::PendingNominations` (r:1 w:1)
|
||||
/// Proof: `Welati::PendingNominations` (`max_values`: None, `max_size`: Some(173), added: 2648, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Welati::NextAppointmentId` (r:1 w:1)
|
||||
/// Proof: `Welati::NextAppointmentId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Welati::AppointmentProcesses` (r:0 w:1)
|
||||
/// Proof: `Welati::AppointmentProcesses` (`max_values`: None, `max_size`: Some(10119), added: 12594, mode: `MaxEncodedLen`)
|
||||
fn nominate_official() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `121`
|
||||
// Estimated: `3638`
|
||||
// Minimum execution time: 25_456_000 picoseconds.
|
||||
Weight::from_parts(26_826_000, 3638)
|
||||
.saturating_add(T::DbWeight::get().reads(5_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(3_u64))
|
||||
}
|
||||
/// Storage: `Welati::CurrentOfficials` (r:1 w:0)
|
||||
/// Proof: `Welati::CurrentOfficials` (`max_values`: None, `max_size`: Some(49), added: 2524, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Welati::AppointmentProcesses` (r:1 w:1)
|
||||
/// Proof: `Welati::AppointmentProcesses` (`max_values`: None, `max_size`: Some(10119), added: 12594, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Welati::PendingNominations` (r:1 w:1)
|
||||
/// Proof: `Welati::PendingNominations` (`max_values`: None, `max_size`: Some(173), added: 2648, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Welati::AppointedOfficials` (r:0 w:1)
|
||||
/// Proof: `Welati::AppointedOfficials` (`max_values`: None, `max_size`: Some(49), added: 2524, mode: `MaxEncodedLen`)
|
||||
fn approve_appointment() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `444`
|
||||
// Estimated: `13584`
|
||||
// Minimum execution time: 28_797_000 picoseconds.
|
||||
Weight::from_parts(30_151_000, 13584)
|
||||
.saturating_add(T::DbWeight::get().reads(3_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(3_u64))
|
||||
}
|
||||
/// Storage: `Welati::ParliamentMembers` (r:1 w:0)
|
||||
/// Proof: `Welati::ParliamentMembers` (`max_values`: Some(1), `max_size`: Some(11057), added: 11552, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Welati::CurrentOfficials` (r:1 w:0)
|
||||
/// Proof: `Welati::CurrentOfficials` (`max_values`: None, `max_size`: Some(49), added: 2524, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Welati::NextProposalId` (r:1 w:1)
|
||||
/// Proof: `Welati::NextProposalId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Welati::ActiveProposals` (r:0 w:1)
|
||||
/// Proof: `Welati::ActiveProposals` (`max_values`: None, `max_size`: Some(1195), added: 3670, mode: `MaxEncodedLen`)
|
||||
fn submit_proposal() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `119`
|
||||
// Estimated: `12542`
|
||||
// Minimum execution time: 16_335_000 picoseconds.
|
||||
Weight::from_parts(17_233_000, 12542)
|
||||
.saturating_add(T::DbWeight::get().reads(3_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(2_u64))
|
||||
}
|
||||
/// Storage: `Welati::ActiveProposals` (r:1 w:1)
|
||||
/// Proof: `Welati::ActiveProposals` (`max_values`: None, `max_size`: Some(1195), added: 3670, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Welati::CollectiveVotes` (r:1 w:1)
|
||||
/// Proof: `Welati::CollectiveVotes` (`max_values`: None, `max_size`: Some(612), added: 3087, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Welati::ParliamentMembers` (r:1 w:0)
|
||||
/// Proof: `Welati::ParliamentMembers` (`max_values`: Some(1), `max_size`: Some(11057), added: 11552, mode: `MaxEncodedLen`)
|
||||
fn vote_on_proposal() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `349`
|
||||
// Estimated: `12542`
|
||||
// Minimum execution time: 20_789_000 picoseconds.
|
||||
Weight::from_parts(21_455_000, 12542)
|
||||
.saturating_add(T::DbWeight::get().reads(3_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(2_u64))
|
||||
}
|
||||
}
|
||||
|
||||
// For backwards compatibility and tests.
|
||||
impl WeightInfo for () {
|
||||
/// Storage: `Welati::NextElectionId` (r:1 w:1)
|
||||
/// Proof: `Welati::NextElectionId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Welati::ActiveElections` (r:0 w:1)
|
||||
/// Proof: `Welati::ActiveElections` (`max_values`: None, `max_size`: Some(29354), added: 31829, mode: `MaxEncodedLen`)
|
||||
fn initiate_election() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `42`
|
||||
// Estimated: `1489`
|
||||
// Minimum execution time: 21_177_000 picoseconds.
|
||||
Weight::from_parts(22_712_000, 1489)
|
||||
.saturating_add(RocksDbWeight::get().reads(1_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(2_u64))
|
||||
}
|
||||
/// Storage: `Welati::ActiveElections` (r:1 w:1)
|
||||
/// Proof: `Welati::ActiveElections` (`max_values`: None, `max_size`: Some(29354), added: 31829, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Welati::ElectionCandidates` (r:1 w:1)
|
||||
/// Proof: `Welati::ElectionCandidates` (`max_values`: None, `max_size`: Some(3833), added: 6308, mode: `MaxEncodedLen`)
|
||||
fn register_candidate() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `145`
|
||||
// Estimated: `32819`
|
||||
// Minimum execution time: 32_272_000 picoseconds.
|
||||
Weight::from_parts(39_628_000, 32819)
|
||||
.saturating_add(RocksDbWeight::get().reads(2_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(2_u64))
|
||||
}
|
||||
/// Storage: `Welati::ActiveElections` (r:1 w:1)
|
||||
/// Proof: `Welati::ActiveElections` (`max_values`: None, `max_size`: Some(29354), added: 31829, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Welati::ElectionVotes` (r:1 w:1)
|
||||
/// Proof: `Welati::ElectionVotes` (`max_values`: None, `max_size`: Some(435), added: 2910, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Welati::ElectionCandidates` (r:1 w:1)
|
||||
/// Proof: `Welati::ElectionCandidates` (`max_values`: None, `max_size`: Some(3833), added: 6308, mode: `MaxEncodedLen`)
|
||||
fn cast_vote() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `3532`
|
||||
// Estimated: `32819`
|
||||
// Minimum execution time: 27_874_000 picoseconds.
|
||||
Weight::from_parts(29_000_000, 32819)
|
||||
.saturating_add(RocksDbWeight::get().reads(3_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(3_u64))
|
||||
}
|
||||
/// Storage: `Welati::ActiveElections` (r:1 w:1)
|
||||
/// Proof: `Welati::ActiveElections` (`max_values`: None, `max_size`: Some(29354), added: 31829, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Welati::ElectionCandidates` (r:1 w:0)
|
||||
/// Proof: `Welati::ElectionCandidates` (`max_values`: None, `max_size`: Some(3833), added: 6308, mode: `MaxEncodedLen`)
|
||||
/// Storage: `IdentityKyc::KycStatuses` (r:2 w:0)
|
||||
/// Proof: `IdentityKyc::KycStatuses` (`max_values`: None, `max_size`: Some(49), added: 2524, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Welati::ElectionResults` (r:0 w:1)
|
||||
/// Proof: `Welati::ElectionResults` (`max_values`: None, `max_size`: Some(6467), added: 8942, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Welati::ParliamentMembers` (r:0 w:1)
|
||||
/// Proof: `Welati::ParliamentMembers` (`max_values`: Some(1), `max_size`: Some(11057), added: 11552, mode: `MaxEncodedLen`)
|
||||
fn finalize_election() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `3682`
|
||||
// Estimated: `32819`
|
||||
// Minimum execution time: 35_614_000 picoseconds.
|
||||
Weight::from_parts(38_634_000, 32819)
|
||||
.saturating_add(RocksDbWeight::get().reads(4_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(3_u64))
|
||||
}
|
||||
/// Storage: `Welati::CurrentOfficials` (r:1 w:0)
|
||||
/// Proof: `Welati::CurrentOfficials` (`max_values`: None, `max_size`: Some(49), added: 2524, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Welati::CurrentMinisters` (r:1 w:0)
|
||||
/// Proof: `Welati::CurrentMinisters` (`max_values`: None, `max_size`: Some(49), added: 2524, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Welati::AppointedOfficials` (r:1 w:0)
|
||||
/// Proof: `Welati::AppointedOfficials` (`max_values`: None, `max_size`: Some(49), added: 2524, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Welati::PendingNominations` (r:1 w:1)
|
||||
/// Proof: `Welati::PendingNominations` (`max_values`: None, `max_size`: Some(173), added: 2648, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Welati::NextAppointmentId` (r:1 w:1)
|
||||
/// Proof: `Welati::NextAppointmentId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Welati::AppointmentProcesses` (r:0 w:1)
|
||||
/// Proof: `Welati::AppointmentProcesses` (`max_values`: None, `max_size`: Some(10119), added: 12594, mode: `MaxEncodedLen`)
|
||||
fn nominate_official() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `121`
|
||||
// Estimated: `3638`
|
||||
// Minimum execution time: 25_456_000 picoseconds.
|
||||
Weight::from_parts(26_826_000, 3638)
|
||||
.saturating_add(RocksDbWeight::get().reads(5_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(3_u64))
|
||||
}
|
||||
/// Storage: `Welati::CurrentOfficials` (r:1 w:0)
|
||||
/// Proof: `Welati::CurrentOfficials` (`max_values`: None, `max_size`: Some(49), added: 2524, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Welati::AppointmentProcesses` (r:1 w:1)
|
||||
/// Proof: `Welati::AppointmentProcesses` (`max_values`: None, `max_size`: Some(10119), added: 12594, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Welati::PendingNominations` (r:1 w:1)
|
||||
/// Proof: `Welati::PendingNominations` (`max_values`: None, `max_size`: Some(173), added: 2648, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Welati::AppointedOfficials` (r:0 w:1)
|
||||
/// Proof: `Welati::AppointedOfficials` (`max_values`: None, `max_size`: Some(49), added: 2524, mode: `MaxEncodedLen`)
|
||||
fn approve_appointment() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `444`
|
||||
// Estimated: `13584`
|
||||
// Minimum execution time: 28_797_000 picoseconds.
|
||||
Weight::from_parts(30_151_000, 13584)
|
||||
.saturating_add(RocksDbWeight::get().reads(3_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(3_u64))
|
||||
}
|
||||
/// Storage: `Welati::ParliamentMembers` (r:1 w:0)
|
||||
/// Proof: `Welati::ParliamentMembers` (`max_values`: Some(1), `max_size`: Some(11057), added: 11552, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Welati::CurrentOfficials` (r:1 w:0)
|
||||
/// Proof: `Welati::CurrentOfficials` (`max_values`: None, `max_size`: Some(49), added: 2524, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Welati::NextProposalId` (r:1 w:1)
|
||||
/// Proof: `Welati::NextProposalId` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Welati::ActiveProposals` (r:0 w:1)
|
||||
/// Proof: `Welati::ActiveProposals` (`max_values`: None, `max_size`: Some(1195), added: 3670, mode: `MaxEncodedLen`)
|
||||
fn submit_proposal() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `119`
|
||||
// Estimated: `12542`
|
||||
// Minimum execution time: 16_335_000 picoseconds.
|
||||
Weight::from_parts(17_233_000, 12542)
|
||||
.saturating_add(RocksDbWeight::get().reads(3_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(2_u64))
|
||||
}
|
||||
/// Storage: `Welati::ActiveProposals` (r:1 w:1)
|
||||
/// Proof: `Welati::ActiveProposals` (`max_values`: None, `max_size`: Some(1195), added: 3670, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Welati::CollectiveVotes` (r:1 w:1)
|
||||
/// Proof: `Welati::CollectiveVotes` (`max_values`: None, `max_size`: Some(612), added: 3087, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Welati::ParliamentMembers` (r:1 w:0)
|
||||
/// Proof: `Welati::ParliamentMembers` (`max_values`: Some(1), `max_size`: Some(11057), added: 11552, mode: `MaxEncodedLen`)
|
||||
fn vote_on_proposal() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `349`
|
||||
// Estimated: `12542`
|
||||
// Minimum execution time: 20_789_000 picoseconds.
|
||||
Weight::from_parts(21_455_000, 12542)
|
||||
.saturating_add(RocksDbWeight::get().reads(3_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(2_u64))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user