From 33b10b75455da5e29f5c4963d8b4fd8a515ebff4 Mon Sep 17 00:00:00 2001 From: DavidHerran Date: Sun, 24 May 2026 22:47:26 -0500 Subject: [PATCH] feat(fees): v0.0.20 pallet-origin-fees scaffold trait-first + vendor OitFeeSchedule Mathi (OCAFB-1..6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 + Config trait + RuntimeEvent - AccountFees> 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 ## 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) --- CHANGELOG.md | 106 +++++ Cargo.lock | 15 + Cargo.toml | 1 + crates/origin-chain-fees/Cargo.toml | 50 +++ crates/origin-chain-fees/src/fee_schedule.rs | 439 +++++++++++++++++++ crates/origin-chain-fees/src/lib.rs | 279 ++++++++++++ crates/origin-chain-types/src/actions.rs | 146 ++++++ crates/origin-chain-types/src/lib.rs | 4 +- crates/origin-chain-types/src/tiers.rs | 51 +++ 9 files changed, 1090 insertions(+), 1 deletion(-) create mode 100644 crates/origin-chain-fees/Cargo.toml create mode 100644 crates/origin-chain-fees/src/fee_schedule.rs create mode 100644 crates/origin-chain-fees/src/lib.rs create mode 100644 crates/origin-chain-types/src/actions.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index b9af44e..d1eafec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,112 @@ by [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versions follow ## [Unreleased] +## [0.0.20] — 2026-05-24 — Phase 2.4 scaffold — pallet-origin-fees trait-first foundation (OCAFB-1..6) + +**🚀 Phase 2.4 FEES pallet SCAFFOLD trait-first foundation** — Vendor copy +Mathi `OitFeeSchedule` trait + matrices + Mock canonical desde commit `ce4fafb` +(2026-05-24 `arkhe-amis-server/src/fee_schedule.rs` 536 LOC). Cross-pillar +economy AMIS↔Origin Chain pallet scaffold canonical pattern análogo Phase 2.1 +(scaffold v0.0.20 → wired v0.0.21 → impl v0.0.22 → Runtime API v0.0.23). + +### Deliverables v0.0.20 + +**New crate canonical `origin-chain-fees`**: +- `crates/origin-chain-fees/Cargo.toml` (NEW): mirror provenance pallet pattern +- `crates/origin-chain-fees/src/lib.rs` (NEW, ~250 LOC): pallet scaffold canonical +- `crates/origin-chain-fees/src/fee_schedule.rs` (NEW, ~400 LOC): vendor copy Mathi + OCAFB-1..6 adapted para consume `origin-chain-types` canonical (ChainAction + + HardwareTier + SubscriptionTier) + +**New canonical types en `origin-chain-types`**: +- `crates/origin-chain-types/src/actions.rs` (NEW, ~130 LOC): `ChainAction` enum + source-of-truth canonical per OCAFB-2 (4 variants Read/Write/Compute/Transfer + con ordinals 0/1/2/3 stable wire format + name() + from_ordinal()) +- `crates/origin-chain-types/src/tiers.rs` (EXTENDED): `SubscriptionTier` enum + mirror Mathi (Free/Pro/Ultra + `multiplier_basis_points()` const fn) +- `crates/origin-chain-types/src/lib.rs`: `pub mod actions;` + `pub use ChainAction` + + `pub use SubscriptionTier` + +**Workspace canonical**: +- `Cargo.toml`: `crates/origin-chain-fees` añadido a members (post-provenance, + pre-bridges) + +**Pallet scaffold surface canonical**: +- `Pallet` struct + `Config` trait (RuntimeEvent canonical) +- `AccountFees>` per-account history canonical +- `FeeHistory { total_paid, action_count }` aggregate stats canonical +- `Event::FeeCharged` + `FeeRefunded` + `EntitlementBlocked` canonical +- `Error::InsufficientBalance` + `EntitlementLocked` + `FeeOverflow` + `AccountNotFound` +- `FeeRejectionReason` enum canonical (mirror Phase 2.1 PROV pattern) +- Scaffold dispatchable `compute_fee_for_action(origin, action, base)` (placeholder + event emission — real impl v0.0.22) + +**Vendor copy OitFeeSchedule canonical** (Mathi commit `ce4fafb`): +- `OitFeeSchedule` trait 4-method dyn-compatible (Send + Sync) per OCAFB-1 +- `subsidy_basis_points(hw, action) -> u32` const fn matrix per OCAFB-3 + Inmutable #16 anti-clasista (Pi 5 EmbeddedArm Write subsidy 9000 vs Ultra 0) +- `entitlement_unlocked(tier, action) -> bool` const fn matrix per OCAFB-4 + Inmutable #37 + #18 (Free + Compute → blocked, Ultra → all unlocked) +- `compute_fee(base, hw, sub, action) -> u64` formula per OCAFB-5 (3-step: + full subsidy → 0, blocked → u64::MAX, basis points math) +- `OitFeeScheduleMock` builder pattern per OCAFB-6 (.with_subscription() + + .with_hardware() + .with_educational_world()) +- `arc_mock()` helper canonical para `Arc` cross-thread + +### OCAFB sub-decisions status post-v0.0.20 + +| OCAFB | Sub-decision | Status | +|-------|--------------|--------| +| OCAFB-1 | OitFeeSchedule trait 4-method dyn-compatible | ✅ vendor copy canonical | +| OCAFB-2 | ChainAction taxonomy 4 variants source-of-truth en types | ✅ **NUEVO canonical en `origin-chain-types::actions`** | +| OCAFB-3 | Subsidy matrix Inmutable #16 anti-clasista | ✅ vendor copy canonical | +| OCAFB-4 | Entitlement matrix Inmutable #37 + #18 | ✅ vendor copy canonical | +| OCAFB-5 | compute_fee formula 3-step | ✅ vendor copy canonical | +| OCAFB-6 | OitFeeScheduleMock test harness | ✅ vendor copy canonical | +| **OCAFB-7** | Pallet-origin-fees consumer Charon | 🚧 **scaffold v0.0.20 → impl v0.0.22 + Runtime API v0.0.23** | + +### Verification end-to-end canonical + +- ✅ `cargo build -p origin-chain-fees` 1.83s clean +- ✅ `cargo test -p origin-chain-fees` — **20 tests verde, 0 failures**: + - 16 vendor copy tests (3 subsidy matrix + 2 entitlement + 5 compute_fee + + 3 mock builder + 3 lobo blanco invariants) + - 4 pallet scaffold tests (compiles + rejection_reason + fee_history_default + + vendor re-export) +- ✅ `cargo test -p origin-chain-types` — **55 tests passing (+8 nuevos)**: + - 6 ChainAction (name + ordinal + roundtrip + unknown + serde + scale) + - 2 SubscriptionTier (multipliers + ordering) +- ✅ Zero new compile errors workspace-wide + +### Cross-pillar consumers status post v0.0.20 + +- ✅ Mathi `OitFeeSchedule` consumed via vendor copy canonical (Charon Phase 2.4 + consumer side scaffold ready) +- ✅ Mathi local `ChainAction` mirror puede swap a `origin_chain_types::ChainAction` + post-OCAFB ratify 7/7 (mi enum es source-of-truth canonical ahora) +- 🟢 Phase 2.4 v0.0.21+ wire pattern ready (Index 8 runtime composition canonical) + +### Roadmap canonical post-v0.0.20 + +| Version | Trigger | +|---------|---------| +| **v0.0.21** | **Wire `Fees` pallet runtime composition Index 8 + RuntimeEvent canonical (sesión 3+ shipping ahora)** | +| v0.0.22 | Real `compute_fee_for_action` impl + storage assertions + mock runtime integration tests E2E (pattern v0.0.18 mirror) | +| v0.0.23 | Runtime API canonical `FeesApi.query_fee_for_action` read-only RPC → **Phase 2.4 COMPLETE 7/7 OCAFB** | +| v0.1.0 | Slice 2e WASM resolved + full testnet Q3 2026 launch | + +### Filosofía canonical sesión 3+ aplicada + +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*". Materializada +compile-detect canonical via lobo blanco invariant +`inmutable_16_embedded_arm_pays_less_than_ultra_lobo_blanco_canonical` — Pi 5 +subsidy 9000 bp strictly > Ultra subsidy 0 bp para non-Read actions. + +"El barquero ferries valor cross-tier sin discriminar. Lobo blanco cuida + Barquero +cobra el óbolo correcto. Mi pallet + Mathi trait = cross-pillar economy AMIS↔Origin +Chain materializada compile-detect canonical." 🐺💎⚓ + ## [0.0.18] — 2026-05-24 — Phase 2.1 mock runtime integration tests E2E (own fixtures, sesión 3+) **Mock runtime canonical + 9 integration tests E2E PROV-3 flow** — Cierra honest disclosure diff --git a/Cargo.lock b/Cargo.lock index 8f19496..e966d78 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2177,6 +2177,21 @@ dependencies = [ "origin-chain-types", ] +[[package]] +name = "origin-chain-fees" +version = "0.0.1" +dependencies = [ + "frame-support", + "frame-system", + "origin-chain-types", + "parity-scale-codec", + "scale-info", + "sp-api", + "sp-core", + "sp-io", + "sp-runtime", +] + [[package]] name = "origin-chain-governance" version = "0.0.1" diff --git a/Cargo.toml b/Cargo.toml index c19b902..d9e5852 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "crates/origin-chain-consensus", "crates/origin-coin-tokenomics", "crates/origin-chain-provenance", + "crates/origin-chain-fees", "crates/origin-chain-bridges", "crates/origin-chain-governance", "crates/origin-chain-runtime", diff --git a/crates/origin-chain-fees/Cargo.toml b/crates/origin-chain-fees/Cargo.toml new file mode 100644 index 0000000..99c0678 --- /dev/null +++ b/crates/origin-chain-fees/Cargo.toml @@ -0,0 +1,50 @@ +[package] +name = "origin-chain-fees" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true +description = "Cross-pillar fee economy pallet canonical per OCAFB-1..6 + Mathi OitFeeSchedule consumer. Phase 2.4 scaffold v0.0.20 trait-first foundation — consumer Mathi commit ce4fafb (arkhe-amis-server::fee_schedule) via vendor copy interim Option B (swap canonical post-OCAFB ratify o post Mathi sub-crate ship)." + +[dependencies] +# Origin Chain canonical types (ChainAction + HardwareTier + SubscriptionTier source-of-truth) +origin-chain-types = { workspace = true } + +# FRAME canonical pallet macro + storage + dispatch + events + errors +frame-support = { workspace = true } +frame-system = { workspace = true } + +# Runtime API canonical (v0.0.23 — FeesApi.query_fee_for_action read-only RPC query) +sp-api = { workspace = true } + +# SCALE codec canonical +parity-scale-codec = { workspace = true } +scale-info = { workspace = true } + +[dev-dependencies] +# Mock runtime canonical para integration tests v0.0.22 — Pattern v0.0.18 mirror. +sp-core = { workspace = true, default-features = true } +sp-io = { workspace = true, default-features = true } +sp-runtime = { workspace = true, default-features = true } + +[features] +default = ["std"] +std = [ + "origin-chain-types/std", + "frame-support/std", + "frame-system/std", + "sp-api/std", + "parity-scale-codec/std", + "scale-info/std", +] +runtime-benchmarks = [ + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", +] +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", +] diff --git a/crates/origin-chain-fees/src/fee_schedule.rs b/crates/origin-chain-fees/src/fee_schedule.rs new file mode 100644 index 0000000..871ed10 --- /dev/null +++ b/crates/origin-chain-fees/src/fee_schedule.rs @@ -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`. +/// +/// **`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` 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` 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`. +pub fn arc_mock(mock: OitFeeScheduleMock) -> Arc { + 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 = 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` cross-thread. + #[test] + fn oit_fee_schedule_trait_object_safe_lobo_blanco_canonical() { + let _schedule: alloc::boxed::Box = 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() {} + assert_send_sync::(); + } + + /// 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" + ); + } + } +} diff --git a/crates/origin-chain-fees/src/lib.rs b/crates/origin-chain-fees/src/lib.rs new file mode 100644 index 0000000..36c9335 --- /dev/null +++ b/crates/origin-chain-fees/src/lib.rs @@ -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` (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` 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> + + IsType<::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(_); + + /// PHASE 2.4 canonical: per-account fee history canonical. + /// + /// `StorageMap` 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 = + 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 { + /// 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 { + /// 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 Pallet { + /// 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, + 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); + } +} diff --git a/crates/origin-chain-types/src/actions.rs b/crates/origin-chain-types/src/actions.rs new file mode 100644 index 0000000..df39a7e --- /dev/null +++ b/crates/origin-chain-types/src/actions.rs @@ -0,0 +1,146 @@ +//! Cross-pillar `ChainAction` canonical enum — OCAFB-2 source-of-truth. +//! +//! Per joint DC `origin_chain_amis_fee_bridge_v1` (`drawer_wing_palace_design_contracts_f024b8611caab85cc8a5ed5a`) +//! OCAFB-2 sub-decision: `origin-chain-types::actions::ChainAction` es upstream +//! source-of-truth canonical post-OCAFB ratify. Mathi `arkhe-amis-server/src/fee_schedule.rs` +//! mantiene local mirror temporal hasta swap canonical post-ratify (pattern +//! análogo DCM-4 `default_material()` Marihan + `HardwareTier` mirror Phanes +//! `arkhe-asset::manifest_validator`). +//! +//! 4 variants canonical {Read, Write, Compute, Transfer} con ordinals 0/1/2/3 +//! stable wire format — adding variants future appende al final preservando +//! ordinals existentes (semver respect canonical). +//! +//! Bidirectional conversion canonical wire-compat via `From` + serde +//! `rename_all="snake_case"` + scale-codec derives canonical. + +use parity_scale_codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen}; +use scale_info::TypeInfo; + +/// Acción cross-pillar 4 variants firme para fee categorization canonical. +/// +/// Per OCAFB-2: source-of-truth canonical en esta crate. Mathi `arkhe-amis-server` +/// mantiene local mirror temporal. Post-OCAFB ratify (deadline 2026-06-21), Mathi +/// swap canonical a `use origin_chain_types::actions::ChainAction`. +/// +/// **Wire format canonical**: +/// - Ordinal `u8`: `0=Read, 1=Write, 2=Compute, 3=Transfer` +/// - Serde: lowercase snake_case (`"read"`, `"write"`, etc.) +/// - SCALE codec: derive Encode/Decode (FRAME-compat para `pallet-origin-fees`) +#[derive( + Debug, Clone, Copy, PartialEq, Eq, Hash, Encode, Decode, DecodeWithMemTracking, MaxEncodedLen, + TypeInfo, serde::Serialize, serde::Deserialize, +)] +#[serde(rename_all = "snake_case")] +pub enum ChainAction { + /// Lectura state (KG triple lookup, asset preview, attestation read). + /// Típicamente free/low-fee per Inmutable #37 Free perpetua. + Read, + /// Mutación state (asset attestation, governance vote, soul.toml update, + /// world spawn). Fee modulated por HardwareTier + SubscriptionTier. + Write, + /// Compute pesado (AGI inference, validation engine pass, perception + /// snapshot, EEG sample). EEG-related variant solo Ultra (Inmutable #18). + Compute, + /// Transfer OGC token (peer-to-peer, fee redirect, governance staking). + /// Free user limitado a basic ≤10 OGC/day per OCAFB-4 entitlement. + Transfer, +} + +impl ChainAction { + /// Nombre lowercase para wire format + logs + telemetría. + pub const fn name(self) -> &'static str { + match self { + ChainAction::Read => "read", + ChainAction::Write => "write", + ChainAction::Compute => "compute", + ChainAction::Transfer => "transfer", + } + } + + /// Ordinal canonical para wire format `u8` (cross-crate sin requerir serde). + /// `0=Read 1=Write 2=Compute 3=Transfer`. Stable order — adding variants + /// future appende al final preservando ordinals existentes (semver respect). + pub const fn ordinal(self) -> u8 { + match self { + ChainAction::Read => 0, + ChainAction::Write => 1, + ChainAction::Compute => 2, + ChainAction::Transfer => 3, + } + } + + /// Parse ordinal canonical → `ChainAction`. Returns `None` para ordinals + /// futuros NO conocidos (forward-compat safe degradation). + pub const fn from_ordinal(ordinal: u8) -> Option { + match ordinal { + 0 => Some(ChainAction::Read), + 1 => Some(ChainAction::Write), + 2 => Some(ChainAction::Compute), + 3 => Some(ChainAction::Transfer), + _ => None, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn chain_action_name_lowercase_canonical() { + assert_eq!(ChainAction::Read.name(), "read"); + assert_eq!(ChainAction::Write.name(), "write"); + assert_eq!(ChainAction::Compute.name(), "compute"); + assert_eq!(ChainAction::Transfer.name(), "transfer"); + } + + #[test] + fn chain_action_ordinal_stable_canonical() { + assert_eq!(ChainAction::Read.ordinal(), 0); + assert_eq!(ChainAction::Write.ordinal(), 1); + assert_eq!(ChainAction::Compute.ordinal(), 2); + assert_eq!(ChainAction::Transfer.ordinal(), 3); + } + + #[test] + fn chain_action_from_ordinal_roundtrip_canonical() { + for action in [ + ChainAction::Read, + ChainAction::Write, + ChainAction::Compute, + ChainAction::Transfer, + ] { + assert_eq!(ChainAction::from_ordinal(action.ordinal()), Some(action)); + } + } + + #[test] + fn chain_action_from_ordinal_unknown_returns_none() { + // Forward-compat: ordinals futuros NO conocidos retornan None safe + assert_eq!(ChainAction::from_ordinal(4), None); + assert_eq!(ChainAction::from_ordinal(255), None); + } + + #[test] + fn chain_action_serde_snake_case_canonical() { + let json = serde_json::to_string(&ChainAction::Read).unwrap(); + assert_eq!(json, "\"read\""); + let parsed: ChainAction = serde_json::from_str("\"compute\"").unwrap(); + assert_eq!(parsed, ChainAction::Compute); + } + + #[test] + fn chain_action_scale_encode_decode_roundtrip_canonical() { + for action in [ + ChainAction::Read, + ChainAction::Write, + ChainAction::Compute, + ChainAction::Transfer, + ] { + let encoded = action.encode(); + let decoded = ChainAction::decode(&mut &encoded[..]).unwrap(); + assert_eq!(decoded, action); + } + } +} diff --git a/crates/origin-chain-types/src/lib.rs b/crates/origin-chain-types/src/lib.rs index 6a490c3..4896d84 100644 --- a/crates/origin-chain-types/src/lib.rs +++ b/crates/origin-chain-types/src/lib.rs @@ -40,6 +40,7 @@ extern crate alloc; pub mod accounts; +pub mod actions; pub mod assets; pub mod attestation; pub mod errors; @@ -51,6 +52,7 @@ pub mod provenance; pub mod tiers; pub use accounts::{AccountId, Signature}; +pub use actions::ChainAction; pub use assets::AssetHash; pub use attestation::{ Blake3Hash, OitIdentity, SeverityTier, ValidationAttestation, ValidationVerdict, @@ -61,7 +63,7 @@ pub use governance::{GovernanceProposal, ProposalId, Vote, VoteStance}; pub use ogc::{Ogc, OgcLedger}; pub use pallets::{OriginFees, OriginProvenance, OriginValidatorEconomy, SlashReason}; pub use provenance::{AgentChain, AgentChainBound, AssetAttestation, ProvenanceLedger}; -pub use tiers::{AssetQualityTier, HardwareTier}; +pub use tiers::{AssetQualityTier, HardwareTier, SubscriptionTier}; /// Bridge trait for cross-chain asset export. Export-first per CHN-5 — Origin /// assets attested locally then bridged outward (Polkadot, Ethereum L2). External diff --git a/crates/origin-chain-types/src/tiers.rs b/crates/origin-chain-types/src/tiers.rs index 11dd5e7..2bf3b7a 100644 --- a/crates/origin-chain-types/src/tiers.rs +++ b/crates/origin-chain-types/src/tiers.rs @@ -77,6 +77,43 @@ impl AssetQualityTier { } } +/// Canonical subscription tier — Mathi sovereign (mirror per ADR-0057). +/// +/// Per OCAFB-4 entitlement matrix + Inmutables #37 (Free perpetua baseline) + +/// #18 (EEG Ultra-only). Used by `pallet-origin-fees` (Charon Phase 2.4) via +/// `OitFeeSchedule` trait. +/// +/// **Mirror canonical**: source-of-truth is `arkhe-amis-core::SubscriptionTier` +/// (Mathi authority Level A). Esta crate re-publica para chain consumption +/// canonical sin dep transitive a arkhe-amis-core. +#[derive( + Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize, +)] +pub enum SubscriptionTier { + /// Free tier perpetua (Inmutable #37). Read ALL + basic Write + basic Transfer + /// (≤10 OGC/day). Compute blocked — upgrade required. + Free, + /// Pro tier — premium subscriber. ALL actions unlocked except EEG Compute + /// (Inmutable #18 Ultra-only). 1.5× fee multiplier base. + Pro, + /// Ultra tier — top premium. ALL actions including EEG Compute (Inmutable #18). + /// 2.0× fee multiplier base. + Ultra, +} + +impl SubscriptionTier { + /// Multiplicador fee basis points per OCAFB-5 formula (10000 = 100% base). + /// Free baseline 10000 (Inmutable #37), Pro premium 15000, Ultra premium 20000. + #[inline] + pub const fn multiplier_basis_points(self) -> u64 { + match self { + SubscriptionTier::Free => 10000, + SubscriptionTier::Pro => 15000, + SubscriptionTier::Ultra => 20000, + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -133,4 +170,18 @@ mod tests { 0 ); } + + #[test] + fn subscription_tier_multipliers_canonical_ocafb_5() { + assert_eq!(SubscriptionTier::Free.multiplier_basis_points(), 10000); + assert_eq!(SubscriptionTier::Pro.multiplier_basis_points(), 15000); + assert_eq!(SubscriptionTier::Ultra.multiplier_basis_points(), 20000); + } + + #[test] + fn subscription_tier_ordering_canonical() { + // Free < Pro < Ultra (canonical ascending order matches paid level) + assert!(SubscriptionTier::Free < SubscriptionTier::Pro); + assert!(SubscriptionTier::Pro < SubscriptionTier::Ultra); + } }