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,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))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user