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,67 @@
[package]
name = "pezpallet-identity-kyc"
version = "1.0.0"
description = "PezkuwiChain Identity and KYC Management Pallet"
authors.workspace = true
homepage.workspace = true
edition.workspace = true
license.workspace = true
publish = false
repository.workspace = true
documentation = "https://docs.rs/pezpallet-identity-kyc"
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
codec = { workspace = true, default-features = false, features = ["derive"] }
pezframe-benchmarking = { optional = true, workspace = true }
pezframe-support = { default-features = false, workspace = true }
pezframe-system = { default-features = false, workspace = true }
log = { default-features = false, workspace = true }
scale-info = { default-features = false, features = [
"derive",
], workspace = true }
pezsp-core = { default-features = false, workspace = true }
pezsp-runtime = { default-features = false, workspace = true }
pezsp-std = { default-features = false, workspace = true }
# Projemizin özel tiplerini ve trait'lerini içeren kütüphane
pezkuwi-primitives = { workspace = true, default-features = false }
[dev-dependencies]
pezpallet-balances = { workspace = true }
pezsp-io = { workspace = true }
[features]
default = ["std"]
std = [
"codec/std",
"pezframe-benchmarking?/std",
"pezframe-support/std",
"pezframe-system/std",
"log/std",
"pezpallet-balances/std",
"pezkuwi-primitives/std",
"scale-info/std",
"pezsp-core/std",
"pezsp-io/std",
"pezsp-runtime/std",
"pezsp-std/std",
]
runtime-benchmarks = [
"pezframe-benchmarking/runtime-benchmarks",
"pezframe-support/runtime-benchmarks",
"pezframe-system/runtime-benchmarks",
"pezpallet-balances/runtime-benchmarks",
"pezkuwi-primitives/runtime-benchmarks",
"pezsp-io/runtime-benchmarks",
"pezsp-runtime/runtime-benchmarks",
]
try-runtime = [
"pezframe-support/try-runtime",
"pezframe-system/try-runtime",
"pezpallet-balances/try-runtime",
"pezsp-runtime/try-runtime",
]
@@ -0,0 +1,136 @@
//! Benchmarking setup for pezpallet-identity-kyc
#![cfg(feature = "runtime-benchmarks")]
use super::*;
use crate::Pallet as IdentityKyc;
use pezframe_benchmarking::v2::*;
use pezframe_support::traits::Currency;
use pezframe_system::RawOrigin;
use pezsp_core::H256;
/// Helper function to create a funded account
fn funded_account<T: Config>(name: &'static str, index: u32) -> T::AccountId {
let caller: T::AccountId = account(name, index, 0);
let amount = T::KycApplicationDeposit::get() * 10u32.into();
T::Currency::make_free_balance_be(&caller, amount);
caller
}
/// Helper function to setup a citizen (for referrer)
fn setup_citizen<T: Config>(who: &T::AccountId) {
KycStatuses::<T>::insert(who, KycLevel::Approved);
}
/// Helper function to setup an applicant in PendingReferral state
fn setup_pending_referral<T: Config>(applicant: &T::AccountId, referrer: &T::AccountId) {
let identity_hash = H256::repeat_byte(0x01);
let application = CitizenshipApplication { identity_hash, referrer: referrer.clone() };
Applications::<T>::insert(applicant, application);
KycStatuses::<T>::insert(applicant, KycLevel::PendingReferral);
// Reserve deposit
let deposit = T::KycApplicationDeposit::get();
let _ = T::Currency::reserve(applicant, deposit);
}
/// Helper function to setup an applicant in ReferrerApproved state
fn setup_referrer_approved<T: Config>(applicant: &T::AccountId, referrer: &T::AccountId) {
let identity_hash = H256::repeat_byte(0x01);
let application = CitizenshipApplication { identity_hash, referrer: referrer.clone() };
Applications::<T>::insert(applicant, application);
KycStatuses::<T>::insert(applicant, KycLevel::ReferrerApproved);
// Reserve deposit
let deposit = T::KycApplicationDeposit::get();
let _ = T::Currency::reserve(applicant, deposit);
}
#[benchmarks]
mod benchmarks {
use super::*;
#[benchmark]
fn apply_for_citizenship() {
let referrer: T::AccountId = funded_account::<T>("referrer", 0);
setup_citizen::<T>(&referrer);
let applicant: T::AccountId = funded_account::<T>("applicant", 1);
let identity_hash = H256::repeat_byte(0x42);
#[extrinsic_call]
apply_for_citizenship(
RawOrigin::Signed(applicant.clone()),
identity_hash,
referrer.clone(),
);
assert_eq!(KycStatuses::<T>::get(&applicant), KycLevel::PendingReferral);
}
#[benchmark]
fn approve_referral() {
let referrer: T::AccountId = funded_account::<T>("referrer", 0);
setup_citizen::<T>(&referrer);
let applicant: T::AccountId = funded_account::<T>("applicant", 1);
setup_pending_referral::<T>(&applicant, &referrer);
#[extrinsic_call]
approve_referral(RawOrigin::Signed(referrer.clone()), applicant.clone());
assert_eq!(KycStatuses::<T>::get(&applicant), KycLevel::ReferrerApproved);
}
#[benchmark]
fn confirm_citizenship() {
let referrer: T::AccountId = funded_account::<T>("referrer", 0);
setup_citizen::<T>(&referrer);
let applicant: T::AccountId = funded_account::<T>("applicant", 1);
setup_referrer_approved::<T>(&applicant, &referrer);
#[extrinsic_call]
confirm_citizenship(RawOrigin::Signed(applicant.clone()));
assert_eq!(KycStatuses::<T>::get(&applicant), KycLevel::Approved);
}
#[benchmark]
fn revoke_citizenship() {
let citizen: T::AccountId = funded_account::<T>("citizen", 0);
setup_citizen::<T>(&citizen);
#[extrinsic_call]
revoke_citizenship(RawOrigin::Root, citizen.clone());
assert_eq!(KycStatuses::<T>::get(&citizen), KycLevel::Revoked);
}
#[benchmark]
fn renounce_citizenship() {
let citizen: T::AccountId = funded_account::<T>("citizen", 0);
setup_citizen::<T>(&citizen);
#[extrinsic_call]
renounce_citizenship(RawOrigin::Signed(citizen.clone()));
assert_eq!(KycStatuses::<T>::get(&citizen), KycLevel::NotStarted);
}
#[benchmark]
fn cancel_application() {
let referrer: T::AccountId = funded_account::<T>("referrer", 0);
setup_citizen::<T>(&referrer);
let applicant: T::AccountId = funded_account::<T>("applicant", 1);
setup_pending_referral::<T>(&applicant, &referrer);
#[extrinsic_call]
cancel_application(RawOrigin::Signed(applicant.clone()));
assert_eq!(KycStatuses::<T>::get(&applicant), KycLevel::NotStarted);
}
impl_benchmark_test_suite!(IdentityKyc, crate::mock::new_test_ext(), crate::mock::Test);
}
@@ -0,0 +1,563 @@
#![cfg_attr(not(feature = "std"), no_std)]
//! # Identity & KYC Pallet - TRUSTLESS MODEL
//!
//! A privacy-preserving, decentralized citizenship verification system.
//!
//! ## Overview
//!
//! This pallet implements a **TRUSTLESS** citizenship verification where:
//! - NO personal data is stored on-chain (only hash)
//! - NO central authority/bot approves applications
//! - Existing citizens vouch for new applicants (referral-based)
//! - Direct responsibility: Referrers are accountable for their referrals
//!
//! ## Security Design (Kurdish People Safety)
//!
//! This system is designed to protect vulnerable populations (like Kurdish people)
//! from hostile regimes that might try to identify applicants:
//! - Only H256 hash of identity stored on-chain
//! - Actual documents stored off-chain (IPFS/encrypted)
//! - No admin can see or leak personal data
//! - Referral chain creates accountability without central authority
//!
//! ## Citizenship Workflow
//!
//! ### 1. Application Phase
//! - User creates identity hash off-chain: `H256(name + email + documents)`
//! - User calls `apply_for_citizenship(identity_hash, referrer_account)`
//! - Referrer MUST be an existing citizen (KycLevel::Approved)
//! - Status changes to `PendingReferral`
//!
//! ### 2. Referrer Approval Phase
//! - Referrer reviews applicant (off-chain verification)
//! - Referrer calls `approve_referral(applicant)` to vouch for them
//! - Status changes to `ReferrerApproved`
//! - Referrer takes personal responsibility for this referral
//!
//! ### 3. Self-Confirmation Phase (Welati NFT Only)
//! - Applicant calls `confirm_citizenship()` to complete the process
//! - Status changes to `Approved`
//! - Citizen NFT (Welati) is minted via self-confirmation
//! - Referral hooks are triggered
//!
//! ## KYC Levels
//!
//! - **NotStarted** - No application submitted
//! - **PendingReferral** - Waiting for referrer approval
//! - **ReferrerApproved** - Referrer approved, ready for self-confirmation
//! - **Approved** - Full citizen with all rights
//! - **Revoked** - Citizenship revoked (governance decision)
//!
//! ## Privacy Features
//!
//! - **Hash-only storage**: No personal data on-chain
//! - **Off-chain documents**: IPFS or encrypted storage
//! - **No admin access**: Decentralized verification
//! - **Referral accountability**: Social trust, not central authority
//!
//! ## Direct Responsibility Model
//!
//! When a citizen is found to be malicious:
//! - ONLY their direct referrer is penalized
//! - Penalty: Trust score reduction + potential citizenship review
//! - Chain reactions are limited to direct relationships
//! - Good referrals from bad actors are NOT penalized
//!
//! ## Interface
//!
//! ### User Extrinsics
//!
//! - `apply_for_citizenship(identity_hash, referrer)` - Submit citizenship application
//! - `confirm_citizenship()` - Self-confirm after referrer approval (Welati only)
//! - `renounce_citizenship()` - Voluntarily give up citizenship
//!
//! ### Referrer Extrinsics
//!
//! - `approve_referral(applicant)` - Vouch for an applicant
//!
//! ### Governance Extrinsics (Root only)
//!
//! - `revoke_citizenship(who)` - Revoke citizenship (governance decision)
//!
//! ## Runtime Integration Example
//!
//! ```ignore
//! impl pezpallet_identity_kyc::Config for Runtime {
//! type RuntimeEvent = RuntimeEvent;
//! type Currency = Balances;
//! type WeightInfo = pezpallet_identity_kyc::weights::BizinikiwiWeight<Runtime>;
//! type OnKycApproved = Referral;
//! type CitizenNftProvider = Tiki;
//! type KycApplicationDeposit = ConstU128<1_000_000_000_000>; // Spam prevention
//! type MaxStringLength = ConstU32<128>;
//! type MaxCidLength = ConstU32<64>;
//! }
//! ```
pub use pallet::*;
pub mod types;
use types::*;
pub mod weights;
pub use weights::WeightInfo;
#[cfg(test)]
mod mock;
#[cfg(test)]
mod tests;
#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;
extern crate alloc;
use pezframe_support::{pezpallet_prelude::*, traits::ReservableCurrency};
use pezframe_system::pezpallet_prelude::*;
use pezsp_core::H256;
#[pezframe_support::pallet]
pub mod pallet {
use super::*;
#[pallet::pallet]
pub struct Pallet<T>(_);
#[pallet::config]
pub trait Config: pezframe_system::Config {
type RuntimeEvent: From<Event<Self>> + IsType<<Self as pezframe_system::Config>::RuntimeEvent>;
type Currency: ReservableCurrency<Self::AccountId>;
/// Origin that can revoke citizenship (governance/root)
type GovernanceOrigin: EnsureOrigin<Self::RuntimeOrigin>;
type WeightInfo: WeightInfo;
/// Hook called when citizenship is approved - used by referral pallet
type OnKycApproved: crate::types::OnKycApproved<Self::AccountId>;
/// Hook called when citizenship is revoked - used by referral pallet for penalty
type OnCitizenshipRevoked: crate::types::OnCitizenshipRevoked<Self::AccountId>;
/// Provider for minting citizen NFTs - used by tiki pallet
type CitizenNftProvider: crate::types::CitizenNftProvider<Self::AccountId>;
/// Deposit required to apply (spam prevention, returned on approval)
#[pallet::constant]
type KycApplicationDeposit: Get<BalanceOf<Self>>;
/// Max string length for legacy storage
#[pallet::constant]
type MaxStringLength: Get<u32>;
/// Max CID length for legacy storage
#[pallet::constant]
type MaxCidLength: Get<u32>;
}
pub type BalanceOf<T> = <<T as Config>::Currency as pezframe_support::traits::Currency<
<T as pezframe_system::Config>::AccountId,
>>::Balance;
// ============= STORAGE =============
/// Citizenship applications (applicant -> application)
/// PRIVACY: Only hash stored, no personal data
#[pallet::storage]
#[pallet::getter(fn applications)]
pub type Applications<T: Config> =
StorageMap<_, Blake2_128Concat, T::AccountId, CitizenshipApplication<T::AccountId>>;
/// Current citizenship status per account
#[pallet::storage]
#[pallet::getter(fn kyc_status_of)]
pub type KycStatuses<T: Config> =
StorageMap<_, Blake2_128Concat, T::AccountId, KycLevel, ValueQuery>;
/// Identity hashes of approved citizens (for verification)
/// Can be used to prove citizenship without revealing identity
#[pallet::storage]
#[pallet::getter(fn identity_hash_of)]
pub type IdentityHashes<T: Config> = StorageMap<_, Blake2_128Concat, T::AccountId, H256>;
/// Referrer of approved citizens (for direct responsibility tracking)
/// Kept permanently for penalty system even after application is removed
#[pallet::storage]
#[pallet::getter(fn citizen_referrer)]
pub type CitizenReferrers<T: Config> =
StorageMap<_, Blake2_128Concat, T::AccountId, T::AccountId>;
// ============= LEGACY STORAGE (for migration) =============
/// Legacy: Identity info storage (deprecated, kept for migration)
#[pallet::storage]
pub type Identities<T: Config> =
StorageMap<_, Blake2_128Concat, T::AccountId, IdentityInfo<T::MaxStringLength>>;
/// Legacy: Pending KYC applications (deprecated, kept for migration)
#[pallet::storage]
pub type PendingKycApplications<T: Config> = StorageMap<
_,
Blake2_128Concat,
T::AccountId,
KycApplication<T::MaxStringLength, T::MaxCidLength>,
>;
// ============= GENESIS CONFIG =============
/// Genesis configuration for bootstrapping initial citizens
/// BOOTSTRAP: Solves chicken-egg problem - first citizens need to exist for others to join
#[pallet::genesis_config]
#[derive(pezframe_support::DefaultNoBound)]
pub struct GenesisConfig<T: Config> {
/// List of founding citizens (AccountId, IdentityHash)
/// These accounts start with Approved status and can accept referrals immediately
pub founding_citizens: alloc::vec::Vec<(T::AccountId, H256)>,
#[serde(skip)]
pub _phantom: core::marker::PhantomData<T>,
}
#[pallet::genesis_build]
impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
fn build(&self) {
// Initialize founding citizens with Approved status
for (account, identity_hash) in &self.founding_citizens {
// Set status to Approved (citizen)
KycStatuses::<T>::insert(account, KycLevel::Approved);
// Store identity hash
IdentityHashes::<T>::insert(account, *identity_hash);
}
}
}
// ============= EVENTS =============
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
/// New citizenship application submitted
CitizenshipApplied { applicant: T::AccountId, referrer: T::AccountId, identity_hash: H256 },
/// Referrer approved the application
ReferralApproved { referrer: T::AccountId, applicant: T::AccountId },
/// Applicant self-confirmed their citizenship (Welati NFT minted)
CitizenshipConfirmed { who: T::AccountId },
/// Citizenship was revoked (by governance)
CitizenshipRevoked { who: T::AccountId },
/// User renounced their citizenship
CitizenshipRenounced { who: T::AccountId },
/// Application was cancelled by the applicant
ApplicationCancelled { who: T::AccountId },
}
// ============= ERRORS =============
#[pallet::error]
pub enum Error<T> {
/// Application already exists for this account
ApplicationAlreadyExists,
/// No application found for this account
ApplicationNotFound,
/// Referrer is not a citizen (must have Approved status)
ReferrerNotCitizen,
/// Cannot refer yourself
SelfReferral,
/// Cannot approve referral in current state (must be PendingReferral)
CannotApproveInCurrentState,
/// Cannot confirm in current state (must be ReferrerApproved)
CannotConfirmInCurrentState,
/// Cannot revoke in current state (must be Approved)
CannotRevokeInCurrentState,
/// User is not a citizen (cannot renounce)
NotACitizen,
/// Only the referrer can approve this application
NotTheReferrer,
/// Cannot cancel application in current state (must be PendingReferral)
CannotCancelInCurrentState,
}
// ============= EXTRINSICS =============
#[pallet::call]
impl<T: Config> Pallet<T> {
/// Apply for citizenship with identity hash and referrer
///
/// TRUSTLESS: No admin involved, referrer vouches for applicant
/// PRIVACY: Only hash stored, actual identity is off-chain
///
/// # Arguments
/// - `identity_hash`: H256 hash of identity documents (calculated off-chain)
/// - `referrer`: Account of existing citizen who will vouch for you
///
/// # Workflow
/// 1. Applicant submits hash + referrer
/// 2. Deposit is reserved (spam prevention)
/// 3. Status becomes PendingReferral
/// 4. Referrer must call approve_referral
#[pallet::call_index(0)]
#[pallet::weight(T::WeightInfo::apply_for_citizenship())]
pub fn apply_for_citizenship(
origin: OriginFor<T>,
identity_hash: H256,
referrer: T::AccountId,
) -> DispatchResult {
let applicant = ensure_signed(origin)?;
// Cannot refer yourself
ensure!(applicant != referrer, Error::<T>::SelfReferral);
// Must not have existing application
ensure!(
KycStatuses::<T>::get(&applicant) == KycLevel::NotStarted,
Error::<T>::ApplicationAlreadyExists
);
// Referrer must be an approved citizen
ensure!(
KycStatuses::<T>::get(&referrer) == KycLevel::Approved,
Error::<T>::ReferrerNotCitizen
);
// Reserve deposit (spam prevention, returned on approval)
let deposit = T::KycApplicationDeposit::get();
T::Currency::reserve(&applicant, deposit)?;
// Store application (only hash, no personal data)
let application = CitizenshipApplication { identity_hash, referrer: referrer.clone() };
Applications::<T>::insert(&applicant, application);
// Update status
KycStatuses::<T>::insert(&applicant, KycLevel::PendingReferral);
Self::deposit_event(Event::CitizenshipApplied { applicant, referrer, identity_hash });
Ok(())
}
/// Referrer approves an applicant's citizenship application
///
/// TRUSTLESS: Referrer takes personal responsibility for this referral
/// ACCOUNTABILITY: If applicant turns out malicious, referrer is penalized
///
/// # Arguments
/// - `applicant`: Account of the person you're vouching for
///
/// # Requirements
/// - Caller must be the referrer specified in the application
/// - Application must be in PendingReferral state
#[pallet::call_index(1)]
#[pallet::weight(T::WeightInfo::approve_referral())]
pub fn approve_referral(origin: OriginFor<T>, applicant: T::AccountId) -> DispatchResult {
let caller = ensure_signed(origin)?;
// Must be in PendingReferral state
ensure!(
KycStatuses::<T>::get(&applicant) == KycLevel::PendingReferral,
Error::<T>::CannotApproveInCurrentState
);
// Get application
let application =
Applications::<T>::get(&applicant).ok_or(Error::<T>::ApplicationNotFound)?;
// Only the referrer can approve
ensure!(application.referrer == caller, Error::<T>::NotTheReferrer);
// Update status to ReferrerApproved
KycStatuses::<T>::insert(&applicant, KycLevel::ReferrerApproved);
Self::deposit_event(Event::ReferralApproved { referrer: caller, applicant });
Ok(())
}
/// Self-confirm citizenship after referrer approval
///
/// TRUSTLESS: Applicant confirms themselves, no admin needed
/// WELATI ONLY: This mints the Citizen NFT via self-confirmation
///
/// # Workflow
/// 1. Deposit is returned
/// 2. Identity hash is stored permanently
/// 3. Status becomes Approved
/// 4. Citizen NFT (Welati) is minted
/// 5. Referral hooks are triggered
#[pallet::call_index(2)]
#[pallet::weight(T::WeightInfo::confirm_citizenship())]
pub fn confirm_citizenship(origin: OriginFor<T>) -> DispatchResult {
let applicant = ensure_signed(origin)?;
// Must be in ReferrerApproved state
ensure!(
KycStatuses::<T>::get(&applicant) == KycLevel::ReferrerApproved,
Error::<T>::CannotConfirmInCurrentState
);
// Get application
let application =
Applications::<T>::take(&applicant).ok_or(Error::<T>::ApplicationNotFound)?;
// Return deposit
let deposit = T::KycApplicationDeposit::get();
T::Currency::unreserve(&applicant, deposit);
// Store identity hash permanently (for proof of citizenship)
IdentityHashes::<T>::insert(&applicant, application.identity_hash);
// Store referrer permanently (for direct responsibility tracking)
// This is needed even after Applications is removed for penalty system
CitizenReferrers::<T>::insert(&applicant, application.referrer.clone());
// Update status to Approved
KycStatuses::<T>::insert(&applicant, KycLevel::Approved);
// Mint citizen NFT with self-confirmation (Welati tiki)
if let Err(e) = T::CitizenNftProvider::mint_citizen_nft_confirmed(&applicant) {
log::warn!("Failed to mint citizen NFT for {:?}: {:?}", applicant, e);
// Don't fail - user is still a citizen
}
// Trigger referral hooks (for referral pallet)
// Pass referrer parameter to avoid data loss between pallets
T::OnKycApproved::on_kyc_approved(&applicant, &application.referrer);
Self::deposit_event(Event::CitizenshipConfirmed { who: applicant });
Ok(())
}
/// Revoke citizenship (governance only)
///
/// Used for malicious actors identified by governance
/// DIRECT RESPONSIBILITY: Triggers penalty for the referrer via referral pallet
#[pallet::call_index(3)]
#[pallet::weight(T::WeightInfo::revoke_citizenship())]
pub fn revoke_citizenship(origin: OriginFor<T>, who: T::AccountId) -> DispatchResult {
T::GovernanceOrigin::ensure_origin(origin)?;
ensure!(
KycStatuses::<T>::get(&who) == KycLevel::Approved,
Error::<T>::CannotRevokeInCurrentState
);
// Update status
KycStatuses::<T>::insert(&who, KycLevel::Revoked);
// Burn citizen NFT
if let Err(e) = T::CitizenNftProvider::burn_citizen_nft(&who) {
log::warn!("Failed to burn citizen NFT for {:?}: {:?}", who, e);
}
// Trigger direct responsibility penalty for the referrer
// This hook notifies the referral pallet to penalize the referrer
T::OnCitizenshipRevoked::on_citizenship_revoked(&who);
Self::deposit_event(Event::CitizenshipRevoked { who });
Ok(())
}
/// Renounce citizenship (voluntary exit)
///
/// Users can freely leave the system
#[pallet::call_index(4)]
#[pallet::weight(T::WeightInfo::renounce_citizenship())]
pub fn renounce_citizenship(origin: OriginFor<T>) -> DispatchResult {
let who = ensure_signed(origin)?;
ensure!(KycStatuses::<T>::get(&who) == KycLevel::Approved, Error::<T>::NotACitizen);
// Burn citizen NFT
T::CitizenNftProvider::burn_citizen_nft(&who)?;
// Reset status
KycStatuses::<T>::insert(&who, KycLevel::NotStarted);
// Remove identity hash
IdentityHashes::<T>::remove(&who);
Self::deposit_event(Event::CitizenshipRenounced { who });
Ok(())
}
/// Cancel pending application and retrieve deposit
///
/// Useful if referrer is unresponsive or user made a mistake.
/// SAFETY: Only works in PendingReferral state (not yet approved)
#[pallet::call_index(5)]
#[pallet::weight(T::WeightInfo::cancel_application())]
pub fn cancel_application(origin: OriginFor<T>) -> DispatchResult {
let applicant = ensure_signed(origin)?;
// Must be in PendingReferral state (not yet approved by referrer)
ensure!(
KycStatuses::<T>::get(&applicant) == KycLevel::PendingReferral,
Error::<T>::CannotCancelInCurrentState
);
// Remove application
Applications::<T>::remove(&applicant);
// Reset status
KycStatuses::<T>::insert(&applicant, KycLevel::NotStarted);
// Unreserve deposit
let deposit = T::KycApplicationDeposit::get();
T::Currency::unreserve(&applicant, deposit);
Self::deposit_event(Event::ApplicationCancelled { who: applicant });
Ok(())
}
}
}
// ============= TRAIT IMPLEMENTATIONS =============
pub use types::KycStatus;
impl<T: Config> types::KycStatus<T::AccountId> for Pallet<T> {
fn get_kyc_status(who: &T::AccountId) -> KycLevel {
KycStatuses::<T>::get(who)
}
}
impl<T: Config> IdentityInfoProvider<T::AccountId, T::MaxStringLength> for Pallet<T> {
fn get_identity_info(who: &T::AccountId) -> Option<IdentityInfo<T::MaxStringLength>> {
// Legacy: Return from old storage if exists
Identities::<T>::get(who)
}
}
/// Helper methods for checking citizenship
impl<T: Config> Pallet<T> {
/// Check if account is a citizen
pub fn is_citizen(who: &T::AccountId) -> bool {
KycStatuses::<T>::get(who) == KycLevel::Approved
}
/// Count total number of citizens
pub fn citizen_count() -> u32 {
KycStatuses::<T>::iter()
.filter(|(_, status)| *status == KycLevel::Approved)
.count() as u32
}
/// Get the referrer of a citizen or applicant
/// Checks both pending applications and approved citizen records
pub fn get_referrer(who: &T::AccountId) -> Option<T::AccountId> {
// First check permanent storage (for approved citizens)
CitizenReferrers::<T>::get(who)
// Then check pending applications
.or_else(|| Applications::<T>::get(who).map(|app| app.referrer))
}
/// Get identity hash of a citizen
pub fn get_identity_hash(who: &T::AccountId) -> Option<H256> {
IdentityHashes::<T>::get(who)
}
}
/// Trait for trust pallet integration
pub trait CitizenshipStatusProvider<AccountId> {
fn is_citizen(who: &AccountId) -> bool;
}
impl<T: Config> CitizenshipStatusProvider<T::AccountId> for Pallet<T> {
fn is_citizen(who: &T::AccountId) -> bool {
KycStatuses::<T>::get(who) == KycLevel::Approved
}
}
@@ -0,0 +1,140 @@
use crate as pezpallet_identity_kyc;
use pezframe_support::{
construct_runtime, derive_impl, parameter_types,
traits::{ConstU128, ConstU32},
};
use pezframe_system::EnsureRoot;
use pezsp_core::H256;
use pezsp_runtime::BuildStorage;
type Block = pezframe_system::mocking::MockBlock<Test>;
pub type AccountId = u64;
pub type Balance = u128;
// Founding citizen for genesis tests
pub const FOUNDER: AccountId = 100;
pub const CITIZEN_1: AccountId = 1;
pub const CITIZEN_2: AccountId = 2;
pub const APPLICANT: AccountId = 3;
construct_runtime!(
pub enum Test
{
System: pezframe_system,
Balances: pezpallet_balances,
IdentityKyc: pezpallet_identity_kyc,
}
);
#[derive_impl(pezframe_system::config_preludes::TestDefaultConfig)]
impl pezframe_system::Config for Test {
type Block = Block;
type AccountData = pezpallet_balances::AccountData<Balance>;
}
#[derive_impl(pezpallet_balances::config_preludes::TestDefaultConfig)]
impl pezpallet_balances::Config for Test {
type Balance = Balance;
type ExistentialDeposit = ConstU128<1>;
type AccountStore = System;
}
parameter_types! {
pub const KycApplicationDepositAmount: Balance = 100;
pub const MaxStringLen: u32 = 50;
pub const MaxCidLen: u32 = 128;
}
// Mock implementation for OnKycApproved hook
// UPDATED: Now includes referrer parameter
pub struct MockOnKycApproved;
impl crate::types::OnKycApproved<AccountId> for MockOnKycApproved {
fn on_kyc_approved(_who: &AccountId, _referrer: &AccountId) {
// No-op for tests - in real runtime this triggers referral pallet
}
}
// Mock implementation for OnCitizenshipRevoked hook
pub struct MockOnCitizenshipRevoked;
impl crate::types::OnCitizenshipRevoked<AccountId> for MockOnCitizenshipRevoked {
fn on_citizenship_revoked(_who: &AccountId) {
// No-op for tests - in real runtime this triggers penalty system
}
}
// Mock implementation for CitizenNftProvider
pub struct MockCitizenNftProvider;
impl crate::types::CitizenNftProvider<AccountId> for MockCitizenNftProvider {
fn mint_citizen_nft(_who: &AccountId) -> pezsp_runtime::DispatchResult {
Ok(())
}
fn mint_citizen_nft_confirmed(_who: &AccountId) -> pezsp_runtime::DispatchResult {
Ok(())
}
fn burn_citizen_nft(_who: &AccountId) -> pezsp_runtime::DispatchResult {
Ok(())
}
}
impl crate::Config for Test {
type RuntimeEvent = RuntimeEvent;
type Currency = Balances;
type GovernanceOrigin = EnsureRoot<Self::AccountId>;
type WeightInfo = ();
type OnKycApproved = MockOnKycApproved;
type OnCitizenshipRevoked = MockOnCitizenshipRevoked;
type CitizenNftProvider = MockCitizenNftProvider;
type KycApplicationDeposit = KycApplicationDepositAmount;
type MaxStringLength = MaxStringLen;
type MaxCidLength = MaxCidLen;
}
/// Build test externalities with founding citizens
pub fn new_test_ext() -> pezsp_io::TestExternalities {
let mut t = pezframe_system::GenesisConfig::<Test>::default().build_storage().unwrap();
pezpallet_balances::GenesisConfig::<Test> {
balances: vec![
(FOUNDER, 1_000_000),
(CITIZEN_1, 10_000),
(CITIZEN_2, 10_000),
(APPLICANT, 10_000),
],
..Default::default()
}
.assimilate_storage(&mut t)
.unwrap();
// Add founding citizen via genesis config
pezpallet_identity_kyc::GenesisConfig::<Test> {
founding_citizens: vec![
(FOUNDER, H256::from_low_u64_be(1)), // Founder is pre-approved
(CITIZEN_1, H256::from_low_u64_be(2)), // Citizen 1 is pre-approved
],
_phantom: Default::default(),
}
.assimilate_storage(&mut t)
.unwrap();
let mut ext = pezsp_io::TestExternalities::new(t);
ext.execute_with(|| System::set_block_number(1));
ext
}
/// Build test externalities without founding citizens (for edge case tests)
pub fn new_test_ext_empty() -> pezsp_io::TestExternalities {
let mut t = pezframe_system::GenesisConfig::<Test>::default().build_storage().unwrap();
pezpallet_balances::GenesisConfig::<Test> {
balances: vec![(FOUNDER, 1_000_000), (CITIZEN_1, 10_000), (APPLICANT, 10_000)],
..Default::default()
}
.assimilate_storage(&mut t)
.unwrap();
let mut ext = pezsp_io::TestExternalities::new(t);
ext.execute_with(|| System::set_block_number(1));
ext
}
@@ -0,0 +1,551 @@
use crate::{mock::*, types::KycLevel, Error, Event};
use pezframe_support::{assert_noop, assert_ok, traits::Currency};
use pezsp_core::H256;
use pezsp_runtime::DispatchError;
// Kolay erişim için paletimize bir takma ad veriyoruz.
type IdentityKycPallet = crate::Pallet<Test>;
// ============================================================================
// Genesis Config Tests
// ============================================================================
#[test]
fn genesis_config_works() {
new_test_ext().execute_with(|| {
// FOUNDER and CITIZEN_1 should be pre-approved via genesis
assert_eq!(IdentityKycPallet::kyc_status_of(FOUNDER), KycLevel::Approved);
assert_eq!(IdentityKycPallet::kyc_status_of(CITIZEN_1), KycLevel::Approved);
// Their identity hashes should be stored
assert!(IdentityKycPallet::identity_hash_of(FOUNDER).is_some());
assert!(IdentityKycPallet::identity_hash_of(CITIZEN_1).is_some());
// Non-founding users should be NotStarted
assert_eq!(IdentityKycPallet::kyc_status_of(APPLICANT), KycLevel::NotStarted);
});
}
// ============================================================================
// apply_for_citizenship Tests
// ============================================================================
#[test]
fn apply_for_citizenship_works() {
new_test_ext().execute_with(|| {
let identity_hash = H256::from_low_u64_be(12345);
// APPLICANT applies with CITIZEN_1 as referrer (who is pre-approved)
assert_ok!(IdentityKycPallet::apply_for_citizenship(
RuntimeOrigin::signed(APPLICANT),
identity_hash,
CITIZEN_1
));
// Check status changed to PendingReferral
assert_eq!(IdentityKycPallet::kyc_status_of(APPLICANT), KycLevel::PendingReferral);
// Check application was stored
let app = IdentityKycPallet::applications(APPLICANT).expect("Application should exist");
assert_eq!(app.identity_hash, identity_hash);
assert_eq!(app.referrer, CITIZEN_1);
// Check deposit was reserved
assert_eq!(Balances::reserved_balance(APPLICANT), KycApplicationDepositAmount::get());
// Check event was emitted
System::assert_last_event(
Event::CitizenshipApplied { applicant: APPLICANT, referrer: CITIZEN_1, identity_hash }
.into(),
);
});
}
#[test]
fn apply_for_citizenship_fails_if_self_referral() {
new_test_ext().execute_with(|| {
// Cannot refer yourself
assert_noop!(
IdentityKycPallet::apply_for_citizenship(
RuntimeOrigin::signed(CITIZEN_1),
H256::from_low_u64_be(999),
CITIZEN_1 // Same as caller
),
Error::<Test>::SelfReferral
);
});
}
#[test]
fn apply_for_citizenship_fails_if_referrer_not_citizen() {
new_test_ext().execute_with(|| {
// APPLICANT is not a citizen, so cannot be a referrer
assert_noop!(
IdentityKycPallet::apply_for_citizenship(
RuntimeOrigin::signed(CITIZEN_2),
H256::from_low_u64_be(999),
APPLICANT // Not a citizen
),
Error::<Test>::ReferrerNotCitizen
);
});
}
#[test]
fn apply_for_citizenship_fails_if_already_applied() {
new_test_ext().execute_with(|| {
let identity_hash = H256::from_low_u64_be(12345);
// First application succeeds
assert_ok!(IdentityKycPallet::apply_for_citizenship(
RuntimeOrigin::signed(APPLICANT),
identity_hash,
CITIZEN_1
));
// Second application fails
assert_noop!(
IdentityKycPallet::apply_for_citizenship(
RuntimeOrigin::signed(APPLICANT),
H256::from_low_u64_be(99999),
CITIZEN_1
),
Error::<Test>::ApplicationAlreadyExists
);
});
}
#[test]
fn apply_for_citizenship_fails_insufficient_balance() {
new_test_ext().execute_with(|| {
let poor_user = 999; // No balance in genesis
assert_noop!(
IdentityKycPallet::apply_for_citizenship(
RuntimeOrigin::signed(poor_user),
H256::from_low_u64_be(12345),
CITIZEN_1
),
pezpallet_balances::Error::<Test>::InsufficientBalance
);
});
}
// ============================================================================
// approve_referral Tests
// ============================================================================
#[test]
fn approve_referral_works() {
new_test_ext().execute_with(|| {
let identity_hash = H256::from_low_u64_be(12345);
// APPLICANT applies with CITIZEN_1 as referrer
assert_ok!(IdentityKycPallet::apply_for_citizenship(
RuntimeOrigin::signed(APPLICANT),
identity_hash,
CITIZEN_1
));
// CITIZEN_1 approves the referral
assert_ok!(IdentityKycPallet::approve_referral(
RuntimeOrigin::signed(CITIZEN_1),
APPLICANT
));
// Check status changed to ReferrerApproved
assert_eq!(IdentityKycPallet::kyc_status_of(APPLICANT), KycLevel::ReferrerApproved);
// Check event
System::assert_last_event(
Event::ReferralApproved { referrer: CITIZEN_1, applicant: APPLICANT }.into(),
);
});
}
#[test]
fn approve_referral_fails_if_not_referrer() {
new_test_ext().execute_with(|| {
// APPLICANT applies with CITIZEN_1 as referrer
assert_ok!(IdentityKycPallet::apply_for_citizenship(
RuntimeOrigin::signed(APPLICANT),
H256::from_low_u64_be(12345),
CITIZEN_1
));
// FOUNDER (different citizen) cannot approve
assert_noop!(
IdentityKycPallet::approve_referral(RuntimeOrigin::signed(FOUNDER), APPLICANT),
Error::<Test>::NotTheReferrer
);
});
}
#[test]
fn approve_referral_fails_if_not_pending() {
new_test_ext().execute_with(|| {
// Try to approve referral for someone who hasn't applied
assert_noop!(
IdentityKycPallet::approve_referral(RuntimeOrigin::signed(CITIZEN_1), APPLICANT),
Error::<Test>::CannotApproveInCurrentState
);
});
}
// ============================================================================
// confirm_citizenship Tests (Self-confirmation for Welati NFT)
// ============================================================================
#[test]
fn confirm_citizenship_works() {
new_test_ext().execute_with(|| {
let identity_hash = H256::from_low_u64_be(12345);
let initial_balance = Balances::free_balance(APPLICANT);
// Apply
assert_ok!(IdentityKycPallet::apply_for_citizenship(
RuntimeOrigin::signed(APPLICANT),
identity_hash,
CITIZEN_1
));
// Referrer approves
assert_ok!(IdentityKycPallet::approve_referral(
RuntimeOrigin::signed(CITIZEN_1),
APPLICANT
));
// Self-confirm
assert_ok!(IdentityKycPallet::confirm_citizenship(RuntimeOrigin::signed(APPLICANT)));
// Check status is Approved
assert_eq!(IdentityKycPallet::kyc_status_of(APPLICANT), KycLevel::Approved);
// Check identity hash is stored permanently
assert_eq!(IdentityKycPallet::identity_hash_of(APPLICANT), Some(identity_hash));
// Check referrer is stored permanently
assert_eq!(IdentityKycPallet::citizen_referrer(APPLICANT), Some(CITIZEN_1));
// Check application was removed
assert!(IdentityKycPallet::applications(APPLICANT).is_none());
// Check deposit was returned
assert_eq!(Balances::reserved_balance(APPLICANT), 0);
assert_eq!(Balances::free_balance(APPLICANT), initial_balance);
// Check event
System::assert_last_event(Event::CitizenshipConfirmed { who: APPLICANT }.into());
});
}
#[test]
fn confirm_citizenship_fails_if_not_referrer_approved() {
new_test_ext().execute_with(|| {
// Apply but don't get referrer approval
assert_ok!(IdentityKycPallet::apply_for_citizenship(
RuntimeOrigin::signed(APPLICANT),
H256::from_low_u64_be(12345),
CITIZEN_1
));
// Try to self-confirm without referrer approval
assert_noop!(
IdentityKycPallet::confirm_citizenship(RuntimeOrigin::signed(APPLICANT)),
Error::<Test>::CannotConfirmInCurrentState
);
});
}
#[test]
fn confirm_citizenship_fails_if_not_applied() {
new_test_ext().execute_with(|| {
// Try to confirm without applying
assert_noop!(
IdentityKycPallet::confirm_citizenship(RuntimeOrigin::signed(APPLICANT)),
Error::<Test>::CannotConfirmInCurrentState
);
});
}
// ============================================================================
// cancel_application Tests
// ============================================================================
#[test]
fn cancel_application_works() {
new_test_ext().execute_with(|| {
let initial_balance = Balances::free_balance(APPLICANT);
// Apply
assert_ok!(IdentityKycPallet::apply_for_citizenship(
RuntimeOrigin::signed(APPLICANT),
H256::from_low_u64_be(12345),
CITIZEN_1
));
// Deposit should be reserved
assert_eq!(Balances::reserved_balance(APPLICANT), KycApplicationDepositAmount::get());
// Cancel
assert_ok!(IdentityKycPallet::cancel_application(RuntimeOrigin::signed(APPLICANT)));
// Status should be reset to NotStarted
assert_eq!(IdentityKycPallet::kyc_status_of(APPLICANT), KycLevel::NotStarted);
// Application should be removed
assert!(IdentityKycPallet::applications(APPLICANT).is_none());
// Deposit should be returned
assert_eq!(Balances::reserved_balance(APPLICANT), 0);
assert_eq!(Balances::free_balance(APPLICANT), initial_balance);
// Event
System::assert_last_event(Event::ApplicationCancelled { who: APPLICANT }.into());
});
}
#[test]
fn cancel_application_fails_if_not_pending_referral() {
new_test_ext().execute_with(|| {
// Apply and get referrer approval
assert_ok!(IdentityKycPallet::apply_for_citizenship(
RuntimeOrigin::signed(APPLICANT),
H256::from_low_u64_be(12345),
CITIZEN_1
));
assert_ok!(IdentityKycPallet::approve_referral(
RuntimeOrigin::signed(CITIZEN_1),
APPLICANT
));
// Cannot cancel after referrer approved (status is ReferrerApproved)
assert_noop!(
IdentityKycPallet::cancel_application(RuntimeOrigin::signed(APPLICANT)),
Error::<Test>::CannotCancelInCurrentState
);
});
}
#[test]
fn cancel_application_allows_reapplication() {
new_test_ext().execute_with(|| {
// First application
assert_ok!(IdentityKycPallet::apply_for_citizenship(
RuntimeOrigin::signed(APPLICANT),
H256::from_low_u64_be(12345),
CITIZEN_1
));
// Cancel
assert_ok!(IdentityKycPallet::cancel_application(RuntimeOrigin::signed(APPLICANT)));
// Can apply again with different referrer
assert_ok!(IdentityKycPallet::apply_for_citizenship(
RuntimeOrigin::signed(APPLICANT),
H256::from_low_u64_be(99999),
FOUNDER // Different referrer this time
));
assert_eq!(IdentityKycPallet::kyc_status_of(APPLICANT), KycLevel::PendingReferral);
});
}
// ============================================================================
// revoke_citizenship Tests (Governance action)
// ============================================================================
#[test]
fn revoke_citizenship_works() {
new_test_ext().execute_with(|| {
// Complete citizenship flow for APPLICANT
assert_ok!(IdentityKycPallet::apply_for_citizenship(
RuntimeOrigin::signed(APPLICANT),
H256::from_low_u64_be(12345),
CITIZEN_1
));
assert_ok!(IdentityKycPallet::approve_referral(
RuntimeOrigin::signed(CITIZEN_1),
APPLICANT
));
assert_ok!(IdentityKycPallet::confirm_citizenship(RuntimeOrigin::signed(APPLICANT)));
assert_eq!(IdentityKycPallet::kyc_status_of(APPLICANT), KycLevel::Approved);
// Governance revokes
assert_ok!(IdentityKycPallet::revoke_citizenship(RuntimeOrigin::root(), APPLICANT));
// Status should be Revoked
assert_eq!(IdentityKycPallet::kyc_status_of(APPLICANT), KycLevel::Revoked);
// Event
System::assert_last_event(Event::CitizenshipRevoked { who: APPLICANT }.into());
});
}
#[test]
fn revoke_citizenship_fails_for_bad_origin() {
new_test_ext().execute_with(|| {
// Non-root cannot revoke
assert_noop!(
IdentityKycPallet::revoke_citizenship(RuntimeOrigin::signed(CITIZEN_1), FOUNDER),
DispatchError::BadOrigin
);
});
}
#[test]
fn revoke_citizenship_fails_if_not_citizen() {
new_test_ext().execute_with(|| {
// APPLICANT is not a citizen
assert_noop!(
IdentityKycPallet::revoke_citizenship(RuntimeOrigin::root(), APPLICANT),
Error::<Test>::CannotRevokeInCurrentState
);
});
}
// ============================================================================
// renounce_citizenship Tests (Voluntary exit)
// ============================================================================
#[test]
fn renounce_citizenship_works() {
new_test_ext().execute_with(|| {
// CITIZEN_1 is pre-approved, can renounce
assert_eq!(IdentityKycPallet::kyc_status_of(CITIZEN_1), KycLevel::Approved);
assert_ok!(IdentityKycPallet::renounce_citizenship(RuntimeOrigin::signed(CITIZEN_1)));
// Status should be reset to NotStarted
assert_eq!(IdentityKycPallet::kyc_status_of(CITIZEN_1), KycLevel::NotStarted);
// Identity hash should be removed
assert!(IdentityKycPallet::identity_hash_of(CITIZEN_1).is_none());
// Event
System::assert_last_event(Event::CitizenshipRenounced { who: CITIZEN_1 }.into());
});
}
#[test]
fn renounce_citizenship_fails_if_not_citizen() {
new_test_ext().execute_with(|| {
// APPLICANT is not a citizen
assert_noop!(
IdentityKycPallet::renounce_citizenship(RuntimeOrigin::signed(APPLICANT)),
Error::<Test>::NotACitizen
);
});
}
// ============================================================================
// Full Workflow Tests
// ============================================================================
#[test]
fn full_citizenship_workflow() {
new_test_ext().execute_with(|| {
let identity_hash = H256::from_low_u64_be(12345);
// 1. Apply
assert_ok!(IdentityKycPallet::apply_for_citizenship(
RuntimeOrigin::signed(APPLICANT),
identity_hash,
CITIZEN_1
));
assert_eq!(IdentityKycPallet::kyc_status_of(APPLICANT), KycLevel::PendingReferral);
// 2. Referrer approves
assert_ok!(IdentityKycPallet::approve_referral(
RuntimeOrigin::signed(CITIZEN_1),
APPLICANT
));
assert_eq!(IdentityKycPallet::kyc_status_of(APPLICANT), KycLevel::ReferrerApproved);
// 3. Self-confirm
assert_ok!(IdentityKycPallet::confirm_citizenship(RuntimeOrigin::signed(APPLICANT)));
assert_eq!(IdentityKycPallet::kyc_status_of(APPLICANT), KycLevel::Approved);
// 4. Now APPLICANT is a citizen and can be a referrer for others
let new_user = 50;
// First give new_user some balance
Balances::make_free_balance_be(&new_user, 10_000);
assert_ok!(IdentityKycPallet::apply_for_citizenship(
RuntimeOrigin::signed(new_user),
H256::from_low_u64_be(99999),
APPLICANT // APPLICANT is now the referrer
));
assert_eq!(IdentityKycPallet::kyc_status_of(new_user), KycLevel::PendingReferral);
});
}
#[test]
fn renounce_and_reapply_workflow() {
new_test_ext().execute_with(|| {
// Complete first citizenship
assert_ok!(IdentityKycPallet::apply_for_citizenship(
RuntimeOrigin::signed(APPLICANT),
H256::from_low_u64_be(12345),
CITIZEN_1
));
assert_ok!(IdentityKycPallet::approve_referral(
RuntimeOrigin::signed(CITIZEN_1),
APPLICANT
));
assert_ok!(IdentityKycPallet::confirm_citizenship(RuntimeOrigin::signed(APPLICANT)));
assert_eq!(IdentityKycPallet::kyc_status_of(APPLICANT), KycLevel::Approved);
// Renounce
assert_ok!(IdentityKycPallet::renounce_citizenship(RuntimeOrigin::signed(APPLICANT)));
assert_eq!(IdentityKycPallet::kyc_status_of(APPLICANT), KycLevel::NotStarted);
// Can reapply (free world principle)
assert_ok!(IdentityKycPallet::apply_for_citizenship(
RuntimeOrigin::signed(APPLICANT),
H256::from_low_u64_be(99999), // Different hash
FOUNDER // Different referrer
));
assert_eq!(IdentityKycPallet::kyc_status_of(APPLICANT), KycLevel::PendingReferral);
});
}
// ============================================================================
// Helper Function Tests
// ============================================================================
#[test]
fn is_citizen_works() {
new_test_ext().execute_with(|| {
// Founding citizens should return true
assert!(IdentityKycPallet::is_citizen(&FOUNDER));
assert!(IdentityKycPallet::is_citizen(&CITIZEN_1));
// Non-citizens should return false
assert!(!IdentityKycPallet::is_citizen(&APPLICANT));
});
}
#[test]
fn get_referrer_works() {
new_test_ext().execute_with(|| {
// Complete citizenship for APPLICANT
assert_ok!(IdentityKycPallet::apply_for_citizenship(
RuntimeOrigin::signed(APPLICANT),
H256::from_low_u64_be(12345),
CITIZEN_1
));
assert_ok!(IdentityKycPallet::approve_referral(
RuntimeOrigin::signed(CITIZEN_1),
APPLICANT
));
assert_ok!(IdentityKycPallet::confirm_citizenship(RuntimeOrigin::signed(APPLICANT)));
// Should return the referrer
assert_eq!(IdentityKycPallet::get_referrer(&APPLICANT), Some(CITIZEN_1));
// Founding citizens have no referrer (they were genesis)
assert_eq!(IdentityKycPallet::get_referrer(&FOUNDER), None);
});
}
@@ -0,0 +1,197 @@
use codec::{Decode, Encode, MaxEncodedLen};
use pezframe_support::pezpallet_prelude::{BoundedVec, Get, RuntimeDebug};
use scale_info::TypeInfo;
use pezsp_core::H256;
/// Citizenship status levels
/// PRIVACY: No personal data stored on-chain, only status and hash
#[derive(
Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen, Copy, Default,
)]
pub enum KycLevel {
/// No citizenship application
#[default]
NotStarted,
/// Application submitted, waiting for referrer approval
/// TRUSTLESS: Referrer must approve before self-confirmation
PendingReferral,
/// Referrer approved, waiting for applicant's self-confirmation
/// TRUSTLESS: No admin involved, applicant confirms themselves
ReferrerApproved,
/// Approved citizen with full rights
Approved,
/// Citizenship revoked (by governance or self-renounce)
Revoked,
}
/// Privacy-preserving citizenship application
/// SECURITY: No personal data on-chain, only hash
#[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
pub struct CitizenshipApplication<AccountId> {
/// Hash of identity documents (actual documents stored off-chain/IPFS)
/// Frontend calculates: H256(name + email + document_cids)
pub identity_hash: H256,
/// The existing citizen who vouches for this applicant
/// TRUSTLESS: Referrer is personally responsible for their referrals
pub referrer: AccountId,
}
#[derive(Encode, Decode, Clone, Default, MaxEncodedLen)]
pub struct IdentityInfo<MaxStringLength: Get<u32>> {
pub name: BoundedVec<u8, MaxStringLength>,
pub email: BoundedVec<u8, MaxStringLength>,
}
// Manually implement PartialEq to avoid requiring `MaxStringLength: PartialEq`
impl<MaxStringLength: Get<u32>> PartialEq for IdentityInfo<MaxStringLength> {
fn eq(&self, other: &Self) -> bool {
self.name == other.name && self.email == other.email
}
}
impl<MaxStringLength: Get<u32>> Eq for IdentityInfo<MaxStringLength> {}
// Manually implement Debug as well for the same reason.
impl<MaxStringLength: Get<u32>> core::fmt::Debug for IdentityInfo<MaxStringLength> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("IdentityInfo")
.field("name", &self.name)
.field("email", &self.email)
.finish()
}
}
impl<MaxStringLength: Get<u32> + 'static> TypeInfo for IdentityInfo<MaxStringLength>
where
BoundedVec<u8, MaxStringLength>: TypeInfo,
{
type Identity = Self;
fn type_info() -> scale_info::Type {
scale_info::Type::builder()
.path(scale_info::Path::new("IdentityInfo", "pezpallet_identity_kyc::types"))
.composite(
scale_info::build::Fields::named()
.field(|f| {
f.ty::<BoundedVec<u8, MaxStringLength>>()
.name("name")
.type_name("BoundedVec<u8, MaxStringLength>")
})
.field(|f| {
f.ty::<BoundedVec<u8, MaxStringLength>>()
.name("email")
.type_name("BoundedVec<u8, MaxStringLength>")
}),
)
}
}
#[derive(Encode, Decode, Clone, Default, MaxEncodedLen)]
pub struct KycApplication<MaxStringLength: Get<u32>, MaxCidLength: Get<u32>> {
pub cids: BoundedVec<BoundedVec<u8, MaxCidLength>, MaxCidLength>,
pub notes: BoundedVec<u8, MaxStringLength>,
}
// Manually implement PartialEq to avoid requiring generic bounds to be PartialEq
impl<MaxStringLength: Get<u32>, MaxCidLength: Get<u32>> PartialEq
for KycApplication<MaxStringLength, MaxCidLength>
{
fn eq(&self, other: &Self) -> bool {
self.cids == other.cids && self.notes == other.notes
}
}
impl<MaxStringLength: Get<u32>, MaxCidLength: Get<u32>> Eq
for KycApplication<MaxStringLength, MaxCidLength>
{
}
// Manually implement Debug as well for the same reason.
impl<MaxStringLength: Get<u32>, MaxCidLength: Get<u32>> core::fmt::Debug
for KycApplication<MaxStringLength, MaxCidLength>
{
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("KycApplication")
.field("cids", &self.cids)
.field("notes", &self.notes)
.finish()
}
}
impl<MaxStringLength: Get<u32> + 'static, MaxCidLength: Get<u32> + 'static> TypeInfo
for KycApplication<MaxStringLength, MaxCidLength>
where
BoundedVec<BoundedVec<u8, MaxCidLength>, MaxCidLength>: TypeInfo,
BoundedVec<u8, MaxStringLength>: TypeInfo,
{
type Identity = Self;
fn type_info() -> scale_info::Type {
scale_info::Type::builder()
.path(scale_info::Path::new("KycApplication", "pezpallet_identity_kyc::types"))
.composite(
scale_info::build::Fields::named()
.field(|f| {
f.ty::<BoundedVec<BoundedVec<u8, MaxCidLength>, MaxCidLength>>()
.name("cids")
.type_name("BoundedVec<BoundedVec<u8, MaxCidLength>, MaxCidLength>")
})
.field(|f| {
f.ty::<BoundedVec<u8, MaxStringLength>>()
.name("notes")
.type_name("BoundedVec<u8, MaxStringLength>")
}),
)
}
}
// --- Dış Dünya İçin Arayüzler (Traits) ---
/// Bir hesabın KYC durumunu sorgulamak için arayüz.
pub trait KycStatus<AccountId> {
fn get_kyc_status(who: &AccountId) -> KycLevel;
}
/// Bir hesabın kimlik bilgilerini sorgulamak için arayüz.
pub trait IdentityInfoProvider<AccountId, MaxStringLength: Get<u32>> {
fn get_identity_info(who: &AccountId) -> Option<IdentityInfo<MaxStringLength>>;
}
/// KYC onaylandığında tetiklenecek eylemleri tanımlayan arayüz.
/// Bu trait identity-kyc palletinde tanımlanır ve diğer palletler (örn. referral)
/// tarafından implement edilir, böylece circular dependency oluşmaz.
///
/// UPDATED (Gemini suggestion): Now includes referrer parameter to avoid
/// data loss when identity-kyc and referral have separate storage.
pub trait OnKycApproved<AccountId> {
/// Called when a citizen is approved
/// - `who`: The newly approved citizen
/// - `referrer`: The citizen who vouched for them (from identity-kyc storage)
fn on_kyc_approved(who: &AccountId, referrer: &AccountId);
}
/// No-op implementation for when no hook is needed
impl<AccountId> OnKycApproved<AccountId> for () {
fn on_kyc_approved(_who: &AccountId, _referrer: &AccountId) {}
}
/// Vatandaşlık NFT'si mintlemek için arayüz.
/// Bu trait identity-kyc palletinde tanımlanır ve tiki pallet tarafından
/// implement edilir, böylece circular dependency oluşmaz.
pub trait CitizenNftProvider<AccountId> {
fn mint_citizen_nft(who: &AccountId) -> pezsp_runtime::DispatchResult;
/// Mint citizen NFT with self-confirmation (uses force_mint internally)
fn mint_citizen_nft_confirmed(who: &AccountId) -> pezsp_runtime::DispatchResult;
/// Burn citizen NFT when user renounces citizenship
fn burn_citizen_nft(who: &AccountId) -> pezsp_runtime::DispatchResult;
}
/// Hook called when citizenship is revoked (for direct responsibility penalty)
/// Defined here to avoid circular dependency, implemented by referral pallet
pub trait OnCitizenshipRevoked<AccountId> {
fn on_citizenship_revoked(who: &AccountId);
}
/// No-op implementation for when no hook is needed
impl<AccountId> OnCitizenshipRevoked<AccountId> for () {
fn on_citizenship_revoked(_who: &AccountId) {}
}
@@ -0,0 +1,245 @@
// This file is part of Bizinikiwi.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Autogenerated weights for `pezpallet_identity_kyc`
//!
//! THIS FILE WAS AUTO-GENERATED USING THE BIZINIKIWI BENCHMARK CLI VERSION 32.0.0
//! DATE: 2025-12-08, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
//! WORST CASE MAP SIZE: `1000000`
//! HOSTNAME: `MamostePC`, CPU: `11th Gen Intel(R) Core(TM) i9-11950H @ 2.60GHz`
//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: `1024`
// Executed Command:
// ./target/release/frame-omni-bencher
// v1
// benchmark
// pallet
// --runtime
// target/release/wbuild/people-pezkuwichain-runtime/people_pezkuwichain_runtime.compact.compressed.wasm
// --pallets
// pezpallet_identity_kyc
// -e
// all
// --steps
// 50
// --repeat
// 20
// --output
// pezcumulus/teyrchains/pezpallets/identity-kyc/src/weights.rs
// --template
// bizinikiwi/.maintain/frame-weight-template.hbs
#![cfg_attr(rustfmt, rustfmt_skip)]
#![allow(unused_parens)]
#![allow(unused_imports)]
#![allow(missing_docs)]
#![allow(dead_code)]
use pezframe_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}};
use core::marker::PhantomData;
/// Weight functions needed for `pezpallet_identity_kyc`.
pub trait WeightInfo {
fn apply_for_citizenship() -> Weight;
fn approve_referral() -> Weight;
fn confirm_citizenship() -> Weight;
fn revoke_citizenship() -> Weight;
fn renounce_citizenship() -> Weight;
fn cancel_application() -> Weight;
}
/// Weights for `pezpallet_identity_kyc` using the Bizinikiwi node and recommended hardware.
pub struct BizinikiwiWeight<T>(PhantomData<T>);
impl<T: pezframe_system::Config> WeightInfo for BizinikiwiWeight<T> {
/// Storage: `IdentityKyc::KycStatuses` (r:2 w:1)
/// Proof: `IdentityKyc::KycStatuses` (`max_values`: None, `max_size`: Some(49), added: 2524, mode: `MaxEncodedLen`)
/// Storage: `System::Account` (r:1 w:1)
/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
/// Storage: `IdentityKyc::Applications` (r:0 w:1)
/// Proof: `IdentityKyc::Applications` (`max_values`: None, `max_size`: Some(112), added: 2587, mode: `MaxEncodedLen`)
fn apply_for_citizenship() -> Weight {
// Proof Size summary in bytes:
// Measured: `253`
// Estimated: `6038`
// Minimum execution time: 32_436_000 picoseconds.
Weight::from_parts(33_789_000, 6038)
.saturating_add(T::DbWeight::get().reads(3_u64))
.saturating_add(T::DbWeight::get().writes(3_u64))
}
/// Storage: `IdentityKyc::KycStatuses` (r:1 w:1)
/// Proof: `IdentityKyc::KycStatuses` (`max_values`: None, `max_size`: Some(49), added: 2524, mode: `MaxEncodedLen`)
/// Storage: `IdentityKyc::Applications` (r:1 w:0)
/// Proof: `IdentityKyc::Applications` (`max_values`: None, `max_size`: Some(112), added: 2587, mode: `MaxEncodedLen`)
fn approve_referral() -> Weight {
// Proof Size summary in bytes:
// Measured: `323`
// Estimated: `3577`
// Minimum execution time: 17_647_000 picoseconds.
Weight::from_parts(18_444_000, 3577)
.saturating_add(T::DbWeight::get().reads(2_u64))
.saturating_add(T::DbWeight::get().writes(1_u64))
}
/// Storage: `IdentityKyc::KycStatuses` (r:1 w:1)
/// Proof: `IdentityKyc::KycStatuses` (`max_values`: None, `max_size`: Some(49), added: 2524, mode: `MaxEncodedLen`)
/// Storage: `IdentityKyc::Applications` (r:1 w:1)
/// Proof: `IdentityKyc::Applications` (`max_values`: None, `max_size`: Some(112), added: 2587, mode: `MaxEncodedLen`)
/// Storage: `System::Account` (r:1 w:1)
/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
/// Storage: `IdentityKyc::IdentityHashes` (r:0 w:1)
/// Proof: `IdentityKyc::IdentityHashes` (`max_values`: None, `max_size`: Some(80), added: 2555, mode: `MaxEncodedLen`)
/// Storage: `IdentityKyc::CitizenReferrers` (r:0 w:1)
/// Proof: `IdentityKyc::CitizenReferrers` (`max_values`: None, `max_size`: Some(80), added: 2555, mode: `MaxEncodedLen`)
fn confirm_citizenship() -> Weight {
// Proof Size summary in bytes:
// Measured: `426`
// Estimated: `3593`
// Minimum execution time: 37_545_000 picoseconds.
Weight::from_parts(40_069_000, 3593)
.saturating_add(T::DbWeight::get().reads(3_u64))
.saturating_add(T::DbWeight::get().writes(5_u64))
}
/// Storage: `IdentityKyc::KycStatuses` (r:1 w:1)
/// Proof: `IdentityKyc::KycStatuses` (`max_values`: None, `max_size`: Some(49), added: 2524, mode: `MaxEncodedLen`)
fn revoke_citizenship() -> Weight {
// Proof Size summary in bytes:
// Measured: `150`
// Estimated: `3514`
// Minimum execution time: 12_919_000 picoseconds.
Weight::from_parts(13_877_000, 3514)
.saturating_add(T::DbWeight::get().reads(1_u64))
.saturating_add(T::DbWeight::get().writes(1_u64))
}
/// Storage: `IdentityKyc::KycStatuses` (r:1 w:1)
/// Proof: `IdentityKyc::KycStatuses` (`max_values`: None, `max_size`: Some(49), added: 2524, mode: `MaxEncodedLen`)
/// Storage: `IdentityKyc::IdentityHashes` (r:0 w:1)
/// Proof: `IdentityKyc::IdentityHashes` (`max_values`: None, `max_size`: Some(80), added: 2555, mode: `MaxEncodedLen`)
fn renounce_citizenship() -> Weight {
// Proof Size summary in bytes:
// Measured: `150`
// Estimated: `3514`
// Minimum execution time: 14_510_000 picoseconds.
Weight::from_parts(14_914_000, 3514)
.saturating_add(T::DbWeight::get().reads(1_u64))
.saturating_add(T::DbWeight::get().writes(2_u64))
}
/// Storage: `IdentityKyc::KycStatuses` (r:1 w:1)
/// Proof: `IdentityKyc::KycStatuses` (`max_values`: None, `max_size`: Some(49), added: 2524, mode: `MaxEncodedLen`)
/// Storage: `System::Account` (r:1 w:1)
/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
/// Storage: `IdentityKyc::Applications` (r:0 w:1)
/// Proof: `IdentityKyc::Applications` (`max_values`: None, `max_size`: Some(112), added: 2587, mode: `MaxEncodedLen`)
fn cancel_application() -> Weight {
// Proof Size summary in bytes:
// Measured: `299`
// Estimated: `3593`
// Minimum execution time: 28_146_000 picoseconds.
Weight::from_parts(29_261_000, 3593)
.saturating_add(T::DbWeight::get().reads(2_u64))
.saturating_add(T::DbWeight::get().writes(3_u64))
}
}
// For backwards compatibility and tests.
impl WeightInfo for () {
/// Storage: `IdentityKyc::KycStatuses` (r:2 w:1)
/// Proof: `IdentityKyc::KycStatuses` (`max_values`: None, `max_size`: Some(49), added: 2524, mode: `MaxEncodedLen`)
/// Storage: `System::Account` (r:1 w:1)
/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
/// Storage: `IdentityKyc::Applications` (r:0 w:1)
/// Proof: `IdentityKyc::Applications` (`max_values`: None, `max_size`: Some(112), added: 2587, mode: `MaxEncodedLen`)
fn apply_for_citizenship() -> Weight {
// Proof Size summary in bytes:
// Measured: `253`
// Estimated: `6038`
// Minimum execution time: 32_436_000 picoseconds.
Weight::from_parts(33_789_000, 6038)
.saturating_add(RocksDbWeight::get().reads(3_u64))
.saturating_add(RocksDbWeight::get().writes(3_u64))
}
/// Storage: `IdentityKyc::KycStatuses` (r:1 w:1)
/// Proof: `IdentityKyc::KycStatuses` (`max_values`: None, `max_size`: Some(49), added: 2524, mode: `MaxEncodedLen`)
/// Storage: `IdentityKyc::Applications` (r:1 w:0)
/// Proof: `IdentityKyc::Applications` (`max_values`: None, `max_size`: Some(112), added: 2587, mode: `MaxEncodedLen`)
fn approve_referral() -> Weight {
// Proof Size summary in bytes:
// Measured: `323`
// Estimated: `3577`
// Minimum execution time: 17_647_000 picoseconds.
Weight::from_parts(18_444_000, 3577)
.saturating_add(RocksDbWeight::get().reads(2_u64))
.saturating_add(RocksDbWeight::get().writes(1_u64))
}
/// Storage: `IdentityKyc::KycStatuses` (r:1 w:1)
/// Proof: `IdentityKyc::KycStatuses` (`max_values`: None, `max_size`: Some(49), added: 2524, mode: `MaxEncodedLen`)
/// Storage: `IdentityKyc::Applications` (r:1 w:1)
/// Proof: `IdentityKyc::Applications` (`max_values`: None, `max_size`: Some(112), added: 2587, mode: `MaxEncodedLen`)
/// Storage: `System::Account` (r:1 w:1)
/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
/// Storage: `IdentityKyc::IdentityHashes` (r:0 w:1)
/// Proof: `IdentityKyc::IdentityHashes` (`max_values`: None, `max_size`: Some(80), added: 2555, mode: `MaxEncodedLen`)
/// Storage: `IdentityKyc::CitizenReferrers` (r:0 w:1)
/// Proof: `IdentityKyc::CitizenReferrers` (`max_values`: None, `max_size`: Some(80), added: 2555, mode: `MaxEncodedLen`)
fn confirm_citizenship() -> Weight {
// Proof Size summary in bytes:
// Measured: `426`
// Estimated: `3593`
// Minimum execution time: 37_545_000 picoseconds.
Weight::from_parts(40_069_000, 3593)
.saturating_add(RocksDbWeight::get().reads(3_u64))
.saturating_add(RocksDbWeight::get().writes(5_u64))
}
/// Storage: `IdentityKyc::KycStatuses` (r:1 w:1)
/// Proof: `IdentityKyc::KycStatuses` (`max_values`: None, `max_size`: Some(49), added: 2524, mode: `MaxEncodedLen`)
fn revoke_citizenship() -> Weight {
// Proof Size summary in bytes:
// Measured: `150`
// Estimated: `3514`
// Minimum execution time: 12_919_000 picoseconds.
Weight::from_parts(13_877_000, 3514)
.saturating_add(RocksDbWeight::get().reads(1_u64))
.saturating_add(RocksDbWeight::get().writes(1_u64))
}
/// Storage: `IdentityKyc::KycStatuses` (r:1 w:1)
/// Proof: `IdentityKyc::KycStatuses` (`max_values`: None, `max_size`: Some(49), added: 2524, mode: `MaxEncodedLen`)
/// Storage: `IdentityKyc::IdentityHashes` (r:0 w:1)
/// Proof: `IdentityKyc::IdentityHashes` (`max_values`: None, `max_size`: Some(80), added: 2555, mode: `MaxEncodedLen`)
fn renounce_citizenship() -> Weight {
// Proof Size summary in bytes:
// Measured: `150`
// Estimated: `3514`
// Minimum execution time: 14_510_000 picoseconds.
Weight::from_parts(14_914_000, 3514)
.saturating_add(RocksDbWeight::get().reads(1_u64))
.saturating_add(RocksDbWeight::get().writes(2_u64))
}
/// Storage: `IdentityKyc::KycStatuses` (r:1 w:1)
/// Proof: `IdentityKyc::KycStatuses` (`max_values`: None, `max_size`: Some(49), added: 2524, mode: `MaxEncodedLen`)
/// Storage: `System::Account` (r:1 w:1)
/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
/// Storage: `IdentityKyc::Applications` (r:0 w:1)
/// Proof: `IdentityKyc::Applications` (`max_values`: None, `max_size`: Some(112), added: 2587, mode: `MaxEncodedLen`)
fn cancel_application() -> Weight {
// Proof Size summary in bytes:
// Measured: `299`
// Estimated: `3593`
// Minimum execution time: 28_146_000 picoseconds.
Weight::from_parts(29_261_000, 3593)
.saturating_add(RocksDbWeight::get().reads(2_u64))
.saturating_add(RocksDbWeight::get().writes(3_u64))
}
}