Refactoring Checkpoint: (WIP)

This commit is contained in:
2025-12-14 10:29:31 +03:00
parent 09735eb97a
commit c89d7cac55
1424 changed files with 6415 additions and 6064 deletions
@@ -0,0 +1,99 @@
[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
documentation = "https://docs.rs/pezpallet-staking-score"
[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/pezpallets/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))
}
}