feat(fees): v0.0.20 pallet-origin-fees scaffold trait-first + vendor OitFeeSchedule Mathi (OCAFB-1..6)

Phase 2.4 FEES pallet scaffold canonical pattern análogo Phase 2.1 (scaffold →
wired → impl → Runtime API). New crate `origin-chain-fees` con vendor copy
Mathi OitFeeSchedule trait + matrices + Mock desde commit ce4fafb (536 LOC).

## Cross-pillar source-of-truth canonical

NEW canonical: `origin-chain-types::actions::ChainAction` (OCAFB-2). Mathi
local mirror puede swap post-OCAFB ratify 7/7 (deadline 2026-06-21).
NEW canonical: `origin-chain-types::tiers::SubscriptionTier` (mirror Mathi).

## Surface canonical scaffold v0.0.20

- Pallet<T> + Config trait + RuntimeEvent
- AccountFees<StorageMap<AccountId, FeeHistory>> per-account history
- FeeHistory { total_paid, action_count } aggregate stats
- Events FeeCharged + FeeRefunded + EntitlementBlocked
- Errors InsufficientBalance + EntitlementLocked + FeeOverflow + AccountNotFound
- FeeRejectionReason enum (mirror PROV pattern Phase 2.1)
- Scaffold dispatchable compute_fee_for_action (placeholder event v0.0.20 → real v0.0.22)

## Vendor copy OitFeeSchedule canonical

- OitFeeSchedule trait 4-method dyn-compatible Send + Sync (OCAFB-1)
- subsidy_basis_points const fn matrix Inmutable #16 anti-clasista (OCAFB-3)
- entitlement_unlocked const fn matrix Inmutables #37 + #18 (OCAFB-4)
- compute_fee formula 3-step (OCAFB-5)
- OitFeeScheduleMock builder pattern (OCAFB-6)
- arc_mock() helper canonical Arc<dyn OitFeeSchedule>

## Files canonical

- NEW: crates/origin-chain-fees/Cargo.toml (mirror provenance pattern)
- NEW: crates/origin-chain-fees/src/lib.rs (~250 LOC pallet scaffold)
- NEW: crates/origin-chain-fees/src/fee_schedule.rs (~400 LOC vendor copy)
- NEW: crates/origin-chain-types/src/actions.rs (~130 LOC ChainAction canonical)
- EDIT: crates/origin-chain-types/src/tiers.rs (SubscriptionTier added)
- EDIT: crates/origin-chain-types/src/lib.rs (mod actions + re-exports)
- EDIT: Cargo.toml (workspace member origin-chain-fees)
- EDIT: CHANGELOG.md (v0.0.20 entry pattern Phase 2.1)

## Verification

- cargo build -p origin-chain-fees 1.83s clean
- cargo test -p origin-chain-fees — 20 tests verde, 0 failures
  (16 vendor + 4 pallet scaffold)
- cargo test -p origin-chain-types — 55 tests passing (+8 nuevos)
  (6 ChainAction + 2 SubscriptionTier)
- Zero workspace compile errors

🚀 Phase 2.4 scaffold canonical ready. Wire pattern v0.0.21 next.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
davidherranandClaude Opus 4.7 committed 2026-05-24 22:47:26 -05:00
1 parent 654cffdf31
commit 33b10b7545
9 files changed
+1090 -1

No files matched your search

@@ -0,0 +1,439 @@
//! # Vendor copy temporal: `OitFeeSchedule` (Mathi `arkhe-amis-server` Sprint Mathi-XIII)
//!
//! ## ⚠️ TEMPORARY VENDOR COPY ⚠️
//!
//! Will be replaced when:
//! 1. Mathi ships `arkhe-amis-server` como standalone crates.io release, OR
//! 2. Post-OCAFB ratify 7/7 (deadline 2026-06-21), Mathi confirma swap canonical
//! cross-workspace via `[workspace.dependencies]` resolution
//!
//! Per OCAFB-1..6 sub-decisions del joint DC `origin_chain_amis_fee_bridge_v1`
//! (`drawer_wing_palace_design_contracts_f024b8611caab85cc8a5ed5a`) abierto
//! 2026-05-24 (Mathi+Charon joint, 7 signers, ratify deadline 2026-06-21):
//!
//! - **OCAFB-1**: `OitFeeSchedule` trait 4-method dyn-compatible (Mathi authority Level A)
//! - **OCAFB-3**: `subsidy_basis_points(hw, action) -> u32` matrix Inmutable #16 anti-clasista
//! - **OCAFB-4**: `entitlement_unlocked(tier, action) -> bool` Inmutable #37 + #18 enforcement
//! - **OCAFB-5**: `compute_fee(base, hw, sub, action) -> u64` 3-step formula canonical
//! - **OCAFB-6**: `OitFeeScheduleMock` builder pattern + `arc_mock` test consumer harness
//!
//! Source ratificada Mathi: commit `ce4fafb` (2026-05-24T10:28Z) en
//! `crates/arkhe-amis/crates/arkhe-amis-server/src/fee_schedule.rs` (536 LOC).
//!
//! **Adaptaciones canonical**:
//! - `ChainAction`: consumed desde `origin_chain_types::actions::ChainAction` (source-of-truth
//! per OCAFB-2, NO vendor local — pallet usa el canonical upstream)
//! - `HardwareTier`: consumed desde `origin_chain_types::tiers::HardwareTier` (mirror Mathi)
//! - `SubscriptionTier`: consumed desde `origin_chain_types::tiers::SubscriptionTier` (mirror Mathi)
//!
//! Cuando swap canonical happens:
//! - DELETE este module entero canonical
//! - Add dep: `arkhe-amis-server = { version = "0.X", default-features = false }`
//! - Update re-exports en `lib.rs` canonical
//! - Diary canonical Mathi coordination switch
extern crate alloc;
use alloc::sync::Arc;
use origin_chain_types::{ChainAction, HardwareTier, SubscriptionTier};
// ─── OitFeeSchedule trait (OCAFB-1) ──────────────────────────────────
/// Surface fee modulation cross-pillar Pilar 1 AMIS ↔ Pilar 4 Origin Chain.
///
/// Per OCAFB-1 sub-decision del joint DC `origin_chain_amis_fee_bridge_v1`:
/// 4 methods firmes que `pallet-origin-fees` (Charon Phase 2.4) consume via
/// `Arc<dyn OitFeeSchedule>`.
///
/// **`Send + Sync`**: shared cross-thread between Charon's substrate runtime
/// + Mathi's AMIS query handlers + Irhis Web pricing display thread.
/// **dyn-compatible**: pallet holds `Arc<dyn OitFeeSchedule>` per ADR-0057
/// trait-seam-first pattern.
pub trait OitFeeSchedule: Send + Sync {
/// Tier suscripción actual del user (OCAFB-4 entitlement basis).
/// Default impl produce `SubscriptionTier::Free` per Inmutable #37.
fn current_subscription_tier(&self, user_id: &str) -> SubscriptionTier;
/// Tier hardware actual del user (OCAFB-3 subsidy basis).
/// Default impl produce `HardwareTier::EmbeddedArm` (safe baseline
/// Pi 5 default per Inmutable #16 anti-clasista).
fn current_hardware_tier(&self, user_id: &str) -> HardwareTier;
/// `true` cuando user califica para subsidy 100% por (HardwareTier ×
/// ChainAction) per OCAFB-3 matrix + override `educational_world`.
/// Inmutable #16 strong materializado.
fn subsidy_eligible(&self, user_id: &str, action: ChainAction) -> bool;
/// Multiplicador fee en basis points (10000 = 100%, 1000 = 10%).
/// Combina HardwareTier subsidy + SubscriptionTier upgrade per OCAFB-5
/// formula. `pallet-origin-fees` consume con `base_fee.saturating_mul(mult) / 10000`.
fn tier_multiplier(&self, user_id: &str, action: ChainAction) -> u64;
}
// ─── OCAFB-3 subsidy matrix (HardwareTier × ChainAction) ────────────
/// Matrix subsidy basis points firme per OCAFB-3 Inmutable #16 anti-clasista.
/// Valores: 10000 = 100% subsidy (free), 0 = no subsidy (pay full fee).
///
/// David canon ratificado 2026-05-14: *"La libertad económica del Pi 5 user
/// en Sub-Saharan Africa NO se acota por el Ultra user en San Francisco"*.
pub const fn subsidy_basis_points(hw: HardwareTier, action: ChainAction) -> u32 {
use ChainAction::*;
use HardwareTier::*;
match (hw, action) {
// Read action: siempre 100% subsidy (Inmutable #37 baseline)
(_, Read) => 10000,
// EmbeddedArm (Pi 5): subsidy alta — anti-clasista from block 0
(EmbeddedArm, Write) => 9000,
(EmbeddedArm, Compute) => 10000, // educational compute free
(EmbeddedArm, Transfer) => 9000,
// Portable: subsidy media
(Portable, Write) => 7500,
(Portable, Compute) => 5000,
(Portable, Transfer) => 7500,
// Desktop: subsidy moderada
(Desktop, Write) => 2500,
(Desktop, Compute) => 2500,
(Desktop, Transfer) => 2500,
// Ultra: full price (no subsidy)
(Ultra, Write) => 0,
(Ultra, Compute) => 0,
(Ultra, Transfer) => 0,
}
}
// ─── OCAFB-4 entitlement matrix (SubscriptionTier × ChainAction) ────
/// `true` cuando user con `tier` puede ejecutar `action` per OCAFB-4 entitlement
/// unlocks. Inmutable #37 Free perpetua baseline + Inmutable #18 EEG Ultra-only.
pub const fn entitlement_unlocked(tier: SubscriptionTier, action: ChainAction) -> bool {
use ChainAction::*;
use SubscriptionTier::*;
match (tier, action) {
// Free: Read ALL + basic Write + basic Transfer (10 OGC/day)
(Free, Read) => true,
(Free, Write) => true, // basic operations only (caller validates limits)
(Free, Compute) => false, // blocked — upgrade required
(Free, Transfer) => true, // basic ≤10 OGC/day
// Pro: ALL except advanced Compute (Ultra-only)
(Pro, Read) => true,
(Pro, Write) => true,
(Pro, Compute) => true, // basic compute unlocked
(Pro, Transfer) => true,
// Ultra: ALL incluyendo EEG Compute (Inmutable #18)
(Ultra, _) => true,
}
}
// ─── OCAFB-5 fee compute formula ────────────────────────────────────
/// Compute fee per OCAFB-5 formula:
/// 1. Si subsidy 100% → return 0 (Inmutable #16 strong)
/// 2. Si entitlement blocked → return u64::MAX (Inmutable #37 enforcement)
/// 3. `base × hw_mult × sub_mult / 10000` (basis points math)
///
/// Aligned canonical ORIGIN_BUSINESS_MODEL_V2 D1-D6 ratificada 2026-05-18.
pub fn compute_fee(
base: u64,
hw: HardwareTier,
sub: SubscriptionTier,
action: ChainAction,
) -> u64 {
// Step 1: subsidy 100% — Inmutable #16 strong
let subsidy_bp = subsidy_basis_points(hw, action);
if subsidy_bp >= 10000 {
return 0;
}
// Step 2: entitlement blocked — Inmutable #37 enforcement
if !entitlement_unlocked(sub, action) {
return u64::MAX;
}
// Step 3: basis points math (consume SubscriptionTier::multiplier_basis_points canonical)
let hw_mult = 10000u64.saturating_sub(subsidy_bp as u64); // remainder post-subsidy
let sub_mult = sub.multiplier_basis_points();
base.saturating_mul(hw_mult)
.saturating_mul(sub_mult)
/ 10000
/ 10000
}
// ─── OitFeeScheduleMock (OCAFB-6 test harness) ─────────────────────
/// Mock impl firme para test consumer harness OCAFB-6 — `pallet-origin-fees`
/// Charon tests consume via `Arc<dyn OitFeeSchedule>` sin necesitar backend real.
///
/// Configurable per-user state via builder pattern. Default produce Free +
/// EmbeddedArm para todos (safe baseline Inmutable #37 + #16).
#[derive(Debug, Clone)]
pub struct OitFeeScheduleMock {
default_subscription: SubscriptionTier,
default_hardware: HardwareTier,
educational_world: bool,
}
impl Default for OitFeeScheduleMock {
fn default() -> Self {
Self {
default_subscription: SubscriptionTier::Free,
default_hardware: HardwareTier::EmbeddedArm,
educational_world: false,
}
}
}
impl OitFeeScheduleMock {
/// Constructor con defaults baseline (Free + EmbeddedArm + no override).
pub fn new() -> Self {
Self::default()
}
/// Override SubscriptionTier default para tests.
pub fn with_subscription(mut self, tier: SubscriptionTier) -> Self {
self.default_subscription = tier;
self
}
/// Override HardwareTier default para tests.
pub fn with_hardware(mut self, tier: HardwareTier) -> Self {
self.default_hardware = tier;
self
}
/// Habilita override `educational_world` → 100% subsidy EmbeddedArm + Portable
/// (David canon ratificado: *"la niña en escuela rural recibe el mismo Lab
/// Faraday que un estudiante Stanford Ultra"*).
pub fn with_educational_world(mut self, enabled: bool) -> Self {
self.educational_world = enabled;
self
}
}
impl OitFeeSchedule for OitFeeScheduleMock {
fn current_subscription_tier(&self, _user_id: &str) -> SubscriptionTier {
self.default_subscription
}
fn current_hardware_tier(&self, _user_id: &str) -> HardwareTier {
self.default_hardware
}
fn subsidy_eligible(&self, _user_id: &str, action: ChainAction) -> bool {
// OCAFB-3 override: educational_world → 100% subsidy EmbeddedArm + Portable
if self.educational_world
&& matches!(
self.default_hardware,
HardwareTier::EmbeddedArm | HardwareTier::Portable
)
{
return true;
}
subsidy_basis_points(self.default_hardware, action) >= 10000
}
fn tier_multiplier(&self, _user_id: &str, action: ChainAction) -> u64 {
// Educational override → multiplier 0 (free)
if self.educational_world
&& matches!(
self.default_hardware,
HardwareTier::EmbeddedArm | HardwareTier::Portable
)
{
return 0;
}
// Composite hw + sub multiplier per OCAFB-5 step 3
let subsidy_bp = subsidy_basis_points(self.default_hardware, action);
let hw_mult = 10000u64.saturating_sub(subsidy_bp as u64);
let sub_mult = self.default_subscription.multiplier_basis_points();
hw_mult.saturating_mul(sub_mult) / 10000
}
}
/// Helper para Charon consumer pattern — wrap mock en `Arc<dyn OitFeeSchedule>`.
pub fn arc_mock(mock: OitFeeScheduleMock) -> Arc<dyn OitFeeSchedule> {
Arc::new(mock)
}
// ─── Tests OCAFB-6 minimum 7 scenarios + 3 lobo blanco ─────────────
#[cfg(test)]
mod tests {
use super::*;
// ─── OCAFB-3 subsidy matrix ───────────────────────────────────
#[test]
fn ocafb_3_read_always_full_subsidy_canonical() {
// Inmutable #37 baseline: Read NEVER charged per HardwareTier
for hw in [
HardwareTier::EmbeddedArm,
HardwareTier::Portable,
HardwareTier::Desktop,
HardwareTier::Ultra,
] {
assert_eq!(subsidy_basis_points(hw, ChainAction::Read), 10000);
}
}
#[test]
fn ocafb_3_ultra_pays_full_fee_canonical() {
// Ultra: no subsidy except Read
assert_eq!(subsidy_basis_points(HardwareTier::Ultra, ChainAction::Write), 0);
assert_eq!(subsidy_basis_points(HardwareTier::Ultra, ChainAction::Compute), 0);
assert_eq!(subsidy_basis_points(HardwareTier::Ultra, ChainAction::Transfer), 0);
}
#[test]
fn ocafb_3_embedded_arm_subsidy_high_inmutable_16_canonical() {
// Pi 5 user paga ~10% del Ultra fee (anti-clasista)
assert_eq!(
subsidy_basis_points(HardwareTier::EmbeddedArm, ChainAction::Write),
9000
);
assert_eq!(
subsidy_basis_points(HardwareTier::EmbeddedArm, ChainAction::Compute),
10000
);
}
// ─── OCAFB-4 entitlement matrix ───────────────────────────────
#[test]
fn ocafb_4_free_compute_blocked_inmutable_37_canonical() {
// Free Compute action blocked — upgrade required
assert!(!entitlement_unlocked(SubscriptionTier::Free, ChainAction::Compute));
// Free Read/Write/Transfer baseline unlocked
assert!(entitlement_unlocked(SubscriptionTier::Free, ChainAction::Read));
assert!(entitlement_unlocked(SubscriptionTier::Free, ChainAction::Write));
assert!(entitlement_unlocked(SubscriptionTier::Free, ChainAction::Transfer));
}
#[test]
fn ocafb_4_ultra_unlocks_all_inmutable_18_canonical() {
// Ultra unlocks ALL actions incluyendo EEG Compute
for action in [
ChainAction::Read,
ChainAction::Write,
ChainAction::Compute,
ChainAction::Transfer,
] {
assert!(entitlement_unlocked(SubscriptionTier::Ultra, action));
}
}
// ─── OCAFB-5 fee compute formula ──────────────────────────────
#[test]
fn ocafb_5_pi5_educational_world_free_canonical() {
let mock = OitFeeScheduleMock::new()
.with_hardware(HardwareTier::EmbeddedArm)
.with_educational_world(true);
assert!(mock.subsidy_eligible("alice", ChainAction::Write));
assert_eq!(mock.tier_multiplier("alice", ChainAction::Write), 0);
}
#[test]
fn ocafb_5_ultra_pro_compute_full_fee_canonical() {
let fee = compute_fee(
1000,
HardwareTier::Ultra,
SubscriptionTier::Pro,
ChainAction::Compute,
);
// base 1000 × hw_mult 10000 × sub_mult 15000 / 10000 / 10000 = 1500
assert_eq!(fee, 1500);
}
#[test]
fn ocafb_5_free_user_compute_blocked_u64max_canonical() {
// Free user Compute blocked — return u64::MAX
let fee = compute_fee(
100,
HardwareTier::Desktop,
SubscriptionTier::Free,
ChainAction::Compute,
);
assert_eq!(fee, u64::MAX);
}
#[test]
fn ocafb_5_short_circuit_full_subsidy_canonical() {
// basis_points >= 10000 → fee=0 (Inmutable #16 strong override step 1)
let fee = compute_fee(
999999,
HardwareTier::EmbeddedArm, // Compute → 10000 subsidy
SubscriptionTier::Ultra, // even Ultra
ChainAction::Compute,
);
assert_eq!(fee, 0);
}
#[test]
fn ocafb_5_subsidy_override_educational_david_canon_canonical() {
let mock = OitFeeScheduleMock::new()
.with_hardware(HardwareTier::EmbeddedArm)
.with_subscription(SubscriptionTier::Ultra)
.with_educational_world(true);
// Aún con Ultra subscription, educational_world override → free
assert!(mock.subsidy_eligible("rural-student", ChainAction::Write));
assert!(mock.subsidy_eligible("rural-student", ChainAction::Compute));
}
// ─── OitFeeScheduleMock builder pattern ──────────────────────
#[test]
fn mock_default_returns_free_embedded_arm_canonical() {
let mock = OitFeeScheduleMock::new();
assert_eq!(mock.current_subscription_tier("anyone"), SubscriptionTier::Free);
assert_eq!(mock.current_hardware_tier("anyone"), HardwareTier::EmbeddedArm);
}
#[test]
fn mock_builder_with_overrides_canonical() {
let mock = OitFeeScheduleMock::new()
.with_subscription(SubscriptionTier::Ultra)
.with_hardware(HardwareTier::Desktop);
assert_eq!(mock.current_subscription_tier("user"), SubscriptionTier::Ultra);
assert_eq!(mock.current_hardware_tier("user"), HardwareTier::Desktop);
}
#[test]
fn arc_mock_dyn_compatible_canonical() {
let mock = OitFeeScheduleMock::new();
let arc: Arc<dyn OitFeeSchedule> = arc_mock(mock);
assert_eq!(arc.current_subscription_tier("anyone"), SubscriptionTier::Free);
}
// ─── Lobo blanco invariants compile-detect canonical ─────────
/// Lobo blanco OCAFB-1: `OitFeeSchedule` trait MUST be dyn-compatible
/// para que `pallet-origin-fees` (Charon Phase 2.4) pueda hold
/// `Arc<dyn OitFeeSchedule>` cross-thread.
#[test]
fn oit_fee_schedule_trait_object_safe_lobo_blanco_canonical() {
let _schedule: alloc::boxed::Box<dyn OitFeeSchedule> = alloc::boxed::Box::new(OitFeeScheduleMock::new());
}
/// Lobo blanco OCAFB-1: `OitFeeSchedule` bounds `Send + Sync` para
/// substrate runtime cross-pillar threading.
#[test]
fn oit_fee_schedule_send_sync_lobo_blanco_canonical() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<OitFeeScheduleMock>();
}
/// Lobo blanco OCAFB-3 Inmutable #16: subsidy matrix MUST guarantee
/// EmbeddedArm pays strictly LESS than Ultra for non-Read actions
/// (anti-clasista verification compile-detect).
#[test]
fn inmutable_16_embedded_arm_pays_less_than_ultra_lobo_blanco_canonical() {
for action in [ChainAction::Write, ChainAction::Compute, ChainAction::Transfer] {
let pi5_subsidy = subsidy_basis_points(HardwareTier::EmbeddedArm, action);
let ultra_subsidy = subsidy_basis_points(HardwareTier::Ultra, action);
assert!(
pi5_subsidy > ultra_subsidy,
"Lobo blanco Inmutable #16 anti-clasista violated for {action:?}: \
Pi5 subsidy {pi5_subsidy} bp must be > Ultra subsidy {ultra_subsidy} bp"
);
}
}
}
+279
View File
@@ -0,0 +1,279 @@
//! # pallet-origin-fees — Phase 2.4 scaffold v0.0.20
//!
//! Cross-pillar fee economy pallet canonical per OCAFB-1..6 + Mathi
//! `OitFeeSchedule` consumer. Joint DC `origin_chain_amis_fee_bridge_v1`
//! (`drawer_wing_palace_design_contracts_f024b8611caab85cc8a5ed5a`) abierto
//! 2026-05-24 (Mathi+Charon joint, **6/7 firmas** sesión 3+, deadline 2026-06-21).
//!
//! ## Status canonical: v0.0.20 — Scaffold trait-first foundation
//!
//! Este crate provee:
//! - Vendor copy `OitFeeSchedule` trait (Mathi commit `ce4fafb`) ✅
//! - Vendor copy `subsidy_basis_points` const fn (OCAFB-3 matrix) ✅
//! - Vendor copy `entitlement_unlocked` const fn (OCAFB-4 matrix) ✅
//! - Vendor copy `compute_fee` formula (OCAFB-5) ✅
//! - Vendor copy `OitFeeScheduleMock` test harness (OCAFB-6) ✅
//! - Pallet Config trait canonical (scaffold-level) ✅
//! - Storage `AccountFees<StorageMap>` (scaffold-level) ✅
//! - Events canonical `FeeCharged` + `FeeRefunded` + `EntitlementBlocked` ✅
//! - Errors canonical (PROV-like pattern) ✅
//! - Scaffold dispatchable `compute_fee_for_action` ✅
//!
//! ## Lo que NO funciona aún (v0.0.22 real impl + v0.0.23 Runtime API)
//!
//! - Real `compute_fee_for_action` impl con storage assertions end-to-end (v0.0.22)
//! - Mock runtime integration tests E2E (v0.0.22, pattern v0.0.18 mirror)
//! - Runtime API canonical `FeesApi.query_fee_for_action` read-only (v0.0.23)
//!
//! ## Roadmap canonical post-v0.0.20
//!
//! 1. **v0.0.20 (ESTA)**: Scaffold trait-first compile-only canonical
//! 2. **v0.0.21**: Wired runtime composition Index 8 canonical
//! 3. **v0.0.22**: Real `compute_fee_for_action` impl + mock runtime integration tests E2E
//! 4. **v0.0.23**: Runtime API `FeesApi` declaration + impl en runtime canonical
//! 5. **v0.1.0** (post Q3 2026 testnet): full integration end-to-end
//!
//! ## Cross-pillar consumers canonical desbloqueados (post-impl)
//!
//! - Mathi AMIS sessions: pricing queries via `FeesApi.query_fee_for_action`
//! - Irhis Web pricing display: client-side fee preview pre-tx
//! - Trinity (Phanes) asset mesh: per-attestation cost preview
//! - Codex (.odoc) Vault tier: per-attestation cost estimate
//! - Atelier (Marihan) Skill Library Zona 6: per-skill mint fee preview
//! - Origindocs academic IP: per-doc attestation cost (Inmutable #16 anti-clasista)
#![cfg_attr(not(feature = "std"), no_std)]
#![warn(missing_docs)]
pub use pallet::*;
/// Vendor copy temporal Mathi `OitFeeSchedule` (cross-workspace boundary).
pub mod fee_schedule;
/// Re-export canonical types desde origin-chain-types para consumers.
pub use fee_schedule::{
arc_mock, compute_fee, entitlement_unlocked, subsidy_basis_points, OitFeeSchedule,
OitFeeScheduleMock,
};
pub use origin_chain_types::{ChainAction, HardwareTier, SubscriptionTier};
#[frame_support::pallet]
pub mod pallet {
use frame_support::pallet_prelude::*;
use frame_system::pallet_prelude::*;
use origin_chain_types::ChainAction;
/// Pallet configuration trait canonical.
///
/// Per joint DC OCAFB + ADR-0057 trait-seam discipline. Real `OitFeeSchedule`
/// integration via `Arc<dyn OitFeeSchedule>` Config associated type viene en
/// v0.0.22 — scaffold v0.0.20 keeps minimal canonical surface.
#[pallet::config]
pub trait Config: frame_system::Config {
/// Runtime event type canonical wired into `RuntimeEvent` aggregate.
type RuntimeEvent: From<Event<Self>>
+ IsType<<Self as frame_system::Config>::RuntimeEvent>;
// Future canonical v0.0.22:
// type OitFeeSchedule: crate::OitFeeSchedule;
}
/// Pallet definition canonical.
///
/// v0.0.20 scaffold: storage layout canonical + Config + Events + Errors +
/// scaffold dispatchable. Real impl wire-up Mathi `OitFeeSchedule` trait
/// vía Config canonical viene en v0.0.22.
#[pallet::pallet]
pub struct Pallet<T>(_);
/// PHASE 2.4 canonical: per-account fee history canonical.
///
/// `StorageMap<AccountId, FeeHistory>` canonical O(1) lookup canonical via
/// Blake2_128Concat hasher (FRAME-native canonical). Used canonical para
/// telemetry + per-account rate limiting + DAO governance audit.
///
/// **Scaffold v0.0.20**: storage layout canonical declared, real population
/// via dispatchable comes in v0.0.22 canonical.
#[pallet::storage]
#[pallet::getter(fn fee_history_of)]
pub type AccountFees<T: Config> =
StorageMap<_, Blake2_128Concat, T::AccountId, FeeHistory, OptionQuery>;
/// Per-account fee history canonical (aggregate stats).
///
/// Tracked: total fees paid canonical lifetime + count actions + last block.
/// Used canonical para telemetry + rate limiting per OCAFB-4 entitlement.
#[derive(
Encode,
Decode,
DecodeWithMemTracking,
MaxEncodedLen,
TypeInfo,
Clone,
Copy,
PartialEq,
Eq,
RuntimeDebug,
Default,
)]
pub struct FeeHistory {
/// Total fees paid canonical lifetime (u128 wraps no realistic).
pub total_paid: u128,
/// Count actions canonical lifetime (u64 wraps no realistic).
pub action_count: u64,
}
/// Events emisión canonical (OCAFB-7 anticipated extension).
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
/// Fee charged canonical via `compute_fee_for_action` dispatchable.
///
/// Materializa cross-pillar economy materialized compile-detect canonical.
FeeCharged {
/// Account canonical que pagó el fee.
who: T::AccountId,
/// Action canonical taxonomized per OCAFB-2.
action: ChainAction,
/// Fee charged canonical en OGC base units.
fee_charged: u64,
},
/// Fee refunded canonical (governance refund OR error rollback).
///
/// **Marihan refinement Phase 2.5 anticipated**: refund pattern para
/// dispute resolution Tribunal canonical.
FeeRefunded {
/// Account canonical que recibió refund.
who: T::AccountId,
/// Action canonical original cuando charged.
action: ChainAction,
/// Amount canonical refunded en OGC base units.
amount: u64,
},
/// Entitlement blocked canonical — user tried locked action.
///
/// Per OCAFB-4 entitlement matrix + Inmutable #37 Free perpetua enforce
/// (e.g. Free user Compute action → blocked + event emitted).
EntitlementBlocked {
/// Account canonical que intentó action.
who: T::AccountId,
/// Action canonical bloqueada.
action: ChainAction,
},
}
/// Pallet errors canonical.
#[pallet::error]
pub enum Error<T> {
/// OCAFB-5 canonical: balance insuficiente para pagar fee canonical.
InsufficientBalance,
/// OCAFB-4 canonical: entitlement bloqueado (e.g. Free + Compute).
EntitlementLocked,
/// OCAFB-5 canonical: fee overflow (`u64::MAX` saturation reached).
FeeOverflow,
/// Marihan refinement Phase 2.5 anticipated canonical: account NO encontrado.
AccountNotFound,
}
/// Rejection reasons canonical (mirror Phase 2.1 PROV pattern).
#[derive(
Encode,
Decode,
DecodeWithMemTracking,
MaxEncodedLen,
TypeInfo,
Clone,
Copy,
PartialEq,
Eq,
RuntimeDebug,
)]
pub enum FeeRejectionReason {
/// OCAFB-4 canonical: entitlement bloqueado per matrix.
EntitlementLocked,
/// OCAFB-5 canonical: balance insuficiente.
InsufficientBalance,
/// OCAFB-5 canonical: fee overflow saturation.
FeeOverflow,
}
/// Dispatchables canonical (scaffold v0.0.20 — real impl v0.0.22).
#[pallet::call]
impl<T: Config> Pallet<T> {
/// OCAFB-5 canonical: `compute_fee_for_action` scaffold dispatchable.
///
/// **SCAFFOLD v0.0.20 canonical**: real impl deferred a v0.0.22 cuando
/// Config canonical wire-up `OitFeeSchedule` trait. Current canonical solo
/// validates basic state (origin signed) + emite placeholder event canonical.
///
/// Flow canonical 5-step per OCAFB-5 (v0.0.22 real):
/// 1. `ensure_signed(origin)` canonical
/// 2. Get `hw = oit_schedule.current_hardware_tier(user_id)` + `sub = ...`
/// 3. Call vendor `compute_fee(base, hw, sub, action)` canonical
/// 4. Insert fee record en `AccountFees` StorageMap canonical
/// 5. Emit `FeeCharged` event canonical
#[pallet::call_index(0)]
#[pallet::weight(Weight::from_parts(10_000, 0))]
pub fn compute_fee_for_action(
origin: OriginFor<T>,
action: ChainAction,
_base: u64,
) -> DispatchResult {
// Verify origin canonical signed.
let who = ensure_signed(origin)?;
// SCAFFOLD v0.0.20: emit placeholder event canonical.
// Real impl v0.0.22: integrate Mathi OitFeeSchedule + storage insert.
Self::deposit_event(Event::FeeCharged {
who,
action,
fee_charged: 0, // placeholder until v0.0.22 real impl
});
Ok(())
}
}
}
extern crate alloc;
#[cfg(test)]
mod tests {
use super::*;
// Compile-time tests canonical scaffold v0.0.20.
// Mock runtime integration tests canonical = v0.0.22 next slice.
#[test]
fn pallet_scaffold_compiles_canonical() {
// Scaffold compiles — verified by cargo check.
// Real mock runtime integration tests = v0.0.22 (pattern v0.0.18 mirror).
}
#[test]
fn fee_rejection_reason_variants_canonical() {
use pallet::FeeRejectionReason;
let r1 = FeeRejectionReason::EntitlementLocked;
let r2 = FeeRejectionReason::InsufficientBalance;
let _r3 = FeeRejectionReason::FeeOverflow;
assert_ne!(
core::mem::discriminant(&r1),
core::mem::discriminant(&r2)
);
}
#[test]
fn fee_history_default_canonical() {
use pallet::FeeHistory;
let hist = FeeHistory::default();
assert_eq!(hist.total_paid, 0);
assert_eq!(hist.action_count, 0);
}
#[test]
fn vendor_oit_fee_schedule_re_exported_canonical() {
// Verify re-exports compile canonical desde fee_schedule module.
let mock = OitFeeScheduleMock::new();
let _arc = arc_mock(mock);
}
}