From e935233ba6ee0eafacdc7bf7a73649089dbda4a5 Mon Sep 17 00:00:00 2001 From: DavidHerran Date: Sun, 24 May 2026 22:56:50 -0500 Subject: [PATCH] =?UTF-8?q?feat(fees):=20v0.0.22=20=E2=80=94=20compute=5Ff?= =?UTF-8?q?ee=5Ffor=5Faction=20impl=20complete=20+=20mock=20runtime=20inte?= =?UTF-8?q?gration=20tests=20E2E=20(10=20scenarios)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 2.4 FEES pallet REAL IMPL canonical. Dispatchable compute_fee_for_action wire-up vendor compute_fee() pure fn call + AccountFees saturating arithmetic + event emission. Mock runtime + 10 integration tests E2E pattern v0.0.18 mirror (Phase 2.1 PROVENANCE). ## Real impl canonical 5-step OCAFB-5 1. ensure_signed canonical 2. Vendor compute_fee(base, hw, sub, action) pure fn call 3. Handle entitlement blocked (fee==u64::MAX → Err(EntitlementLocked)) 4. AccountFees saturating update canonical 5. Emit FeeCharged event canonical Pure helper Pallet::::query_fee() para Runtime API v0.0.23 consumer. ## Consumer pattern canonical Mathi OitFeeSchedule consumed CLIENT-SIDE — tx submitter queries Mathi per-user → assembles tx con (hw, sub) baked → submits. Pattern análogo Phase 2.1 attest_asset (ValidationAttestation pre-assembled client-side). ## 10 integration tests E2E - scenario_1: Pi5 Write subsidy 90% → fee=1000 (Inmutable #16) - scenario_2: Ultra Write Pro multiplier → fee=1500 - scenario_3: Read always free → fee=0 (Inmutable #37) - scenario_4: Free Compute blocked → Err(EntitlementLocked) - scenario_5: Pro Compute unlocked → fee=112 - scenario_6: Ultra Compute Ultra premium → fee=2000 (Inmutable #18) - scenario_7: Storage accumulates Write×3 → total_paid=3375, count=3 - scenario_8: ChainAction ordinal stable wire (Read=0..Transfer=3) - scenario_9: Inmutable #16 lobo blanco — Pi5 fee 1000 < Ultra fee 20000 - scenario_10: 5-step OCAFB-5 walkthrough explicit Pre/Post assertions ## FRAME canonical pattern Events NOT emitted on Err path (FRAME revierte events on dispatch error). EntitlementBlocked event variant retained para Phase 2.5 future with_storage_layer pattern. Caller infers from Err return. ## Files canonical - EDIT: crates/origin-chain-fees/src/lib.rs (real dispatchable impl + query_fee helper) - NEW: crates/origin-chain-fees/src/mock.rs (80 LOC mock runtime pattern v0.0.18) - NEW: crates/origin-chain-fees/src/tests_integration.rs (290 LOC, 10 tests) - EDIT: crates/origin-chain-types/src/tiers.rs (FRAME derives Encode/Decode/TypeInfo/MaxEncodedLen para HardwareTier + SubscriptionTier + AssetQualityTier) - EDIT: CHANGELOG.md (v0.0.22 entry comprehensive) ## Verification - cargo test -p origin-chain-fees — 32 tests verde, 0 failures - cargo test --workspace — 137 tests verde workspace, 0 failures - Zero compile errors workspace-wide 🛠️ Phase 2.4 real impl canonical. Runtime API v0.0.23 next ship. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 97 ++++++ crates/origin-chain-fees/src/lib.rs | 119 +++++-- crates/origin-chain-fees/src/mock.rs | 76 +++++ .../src/tests_integration.rs | 303 ++++++++++++++++++ crates/origin-chain-types/src/tiers.rs | 9 + 5 files changed, 570 insertions(+), 34 deletions(-) create mode 100644 crates/origin-chain-fees/src/mock.rs create mode 100644 crates/origin-chain-fees/src/tests_integration.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 08d8ae7..11b0b1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,103 @@ by [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versions follow ## [Unreleased] +## [0.0.22] — 2026-05-24 — Phase 2.4 impl — Real `compute_fee_for_action` + mock runtime integration tests E2E (10 scenarios) + +**🛠️ Phase 2.4 FEES pallet REAL IMPL canonical** — `compute_fee_for_action` +dispatchable real wired via vendor `compute_fee()` pure fn call + storage update +`AccountFees` saturating arithmetic + event emission canonical. Mock +runtime + 10 integration tests E2E pattern v0.0.18 mirror (Phase 2.1 PROVENANCE). + +### Deliverables v0.0.22 + +**Real dispatchable canonical**: +- `crates/origin-chain-fees/src/lib.rs::pallet::compute_fee_for_action` signature + canonical: `(origin, action, base, hw, sub) -> DispatchResult` +- Flow canonical 5-step per OCAFB-5: + 1. `ensure_signed(origin)` canonical + 2. Vendor `compute_fee(base, hw, sub, action)` pure fn call canonical (3-step formula) + 3. Handle entitlement blocked canonical (`fee == u64::MAX` → `Err(EntitlementLocked)`) + 4. `AccountFees` update saturating arithmetic canonical + 5. Emit `FeeCharged` event canonical +- Pure helper `Pallet::::query_fee(action, base, hw, sub) -> u64` para Runtime API + v0.0.23 consumer (wraps vendor compute_fee read-only canonical) + +**Consumer pattern canonical**: +- Mathi `OitFeeSchedule` trait consumed CLIENT-SIDE (off-chain) — tx submitter + queries Mathi per-user → assembles tx con `hw + sub` baked → submits canonical +- Pattern análogo Phase 2.1 `attest_asset(origin, ValidationAttestation)` (validation + pre-assembled by Noesis client-side, pallet consumes serialized struct directly) + +**Mock runtime canonical (NEW file)**: +- `crates/origin-chain-fees/src/mock.rs` — 80 LOC pattern v0.0.18 mirror +- `construct_runtime!` macro: `System` + `Fees` (minimal testing) +- `frame_system::Config` impl via `derive_impl(TestDefaultConfig)` + `AccountId32` +- `new_test_ext()` helper canonical (`sp_io::TestExternalities` con block 1) +- Helpers `alice_origin`/`alice_account` + `bob_origin`/`bob_account` + +**Integration tests canonical E2E (NEW file)**: +- `crates/origin-chain-fees/src/tests_integration.rs` — 290 LOC, **10 tests verde canonical** + +**10 integration tests OCAFB-5 flow canonical**: + +| # | Scenario | HW | Sub | Action | Expected outcome | +|---|----------|----|----|--------|------------------| +| 1 | `scenario_1_pi5_write_subsidy_90_canonical` | EmbeddedArm | Free | Write | fee=1000 (10% del base 10000, Inmutable #16) | +| 2 | `scenario_2_ultra_write_full_price_canonical` | Ultra | Pro | Write | fee=1500 (base 1000 × 1.5 Pro multiplier) | +| 3 | `scenario_3_read_always_free_inmutable_37_canonical` | Desktop | Free | Read | fee=0 (subsidy 100%, Inmutable #37) | +| 4 | `scenario_4_free_compute_blocked_entitlement_canonical` | Desktop | Free | Compute | `Err(EntitlementLocked)` (Inmutable #37 enforcement) | +| 5 | `scenario_5_pro_compute_unlocked_canonical` | Desktop | Pro | Compute | fee=112 (basis points math) | +| 6 | `scenario_6_ultra_compute_premium_canonical` | Ultra | Ultra | Compute | fee=2000 (base × 2.0 Ultra premium, Inmutable #18) | +| 7 | `scenario_7_storage_accumulates_canonical` | Desktop | Pro | Write × 3 | history.total_paid=3375 + count=3 | +| 8 | `scenario_8_chain_action_ordinal_stable_wire_canonical` | n/a | n/a | wire invariant | Read=0 + Write=1 + Compute=2 + Transfer=3 + roundtrip | +| 9 | `scenario_9_inmutable_16_anti_clasista_lobo_blanco_canonical` | Pi5/Ultra | Free/Ultra | Write × 2 | Pi5 fee 1000 STRICTLY < Ultra fee 20000 (compile-detect) | +| 10 | `scenario_10_5_step_walkthrough_canonical` | EmbeddedArm | Free | Transfer | explicit Pre/Post-flight OCAFB-5 assertions | + +**FRAME canonical pattern decision**: +- Events NOT emitted on Err path (FRAME revierte events on dispatch error) +- `EntitlementBlocked` event variant retained en enum para Phase 2.5 future + `with_storage_layer` pattern (preserve events on error vía dispatch transaction) +- Caller infers blocked entitlement from `Err(EntitlementLocked)` return value + +### OCAFB sub-decisions status post-v0.0.22 + +| OCAFB | Sub-decision | Status | +|-------|--------------|--------| +| OCAFB-1 | OitFeeSchedule trait 4-method dyn-compatible | ✅ vendor canonical | +| OCAFB-2 | ChainAction taxonomy source-of-truth en types | ✅ | +| OCAFB-3 | Subsidy matrix Inmutable #16 anti-clasista | ✅ wired + lobo blanco verified | +| OCAFB-4 | Entitlement matrix Inmutable #37 + #18 | ✅ enforced via dispatchable | +| OCAFB-5 | compute_fee formula 3-step canonical | ✅ **REAL IMPL dispatchable** | +| OCAFB-6 | OitFeeScheduleMock test harness | ✅ vendor + mock runtime tests E2E | +| **OCAFB-7** | Pallet-origin-fees consumer Charon | 🚧 **storage + dispatchable + tests verde — Runtime API v0.0.23 next** | + +### Verification end-to-end canonical + +- ✅ `cargo test -p origin-chain-fees` — **32 tests verde canonical, 0 failures**: + - 10 integration tests E2E (`tests_integration` module) + - 16 vendor copy tests (`fee_schedule::tests` module) + - 4 pallet standalone tests (`tests` module) + - 2 auto-generated (mock genesis + runtime integrity) +- ✅ `cargo test --workspace` — **137 tests verde workspace canonical, 0 failures** +- ✅ Zero new compile errors workspace-wide +- ✅ FRAME-required derives canonical added a `HardwareTier` + `SubscriptionTier` + + `AssetQualityTier` (Encode + Decode + DecodeWithMemTracking + MaxEncodedLen + TypeInfo) + +### Inmutable #16 anti-clasista canonical compile-detect + +Lobo blanco invariant verified via integration test `scenario_9_inmutable_16_anti_clasista_lobo_blanco_canonical`: +- Pi 5 EmbeddedArm Free Write canonical: fee=1000 (10% del base 10000) +- Ultra Ultra Write canonical: fee=20000 (200% del base) +- Ratio canonical 1:20 — Pi 5 user paga STRICTAMENTE menos que Ultra (David canon + ratificado 2026-05-14 materializado canonical en running code) + +### Roadmap canonical post-v0.0.22 + +| Version | Trigger | +|---------|---------| +| **v0.0.23** | **Runtime API canonical `FeesApi.query_fee_for_action` read-only RPC declaration + impl en runtime → Phase 2.4 COMPLETE 7/7 OCAFB (sesión 3+ shipping ahora)** | +| v0.1.0 | Slice 2e WASM resolved + full testnet Q3 2026 launch | + ## [0.0.21] — 2026-05-24 — Phase 2.4 wired — Fees pallet runtime composition Index 8 **🔌 Phase 2.4 FEES pallet WIRED runtime composition canonical** — `origin-chain-fees` diff --git a/crates/origin-chain-fees/src/lib.rs b/crates/origin-chain-fees/src/lib.rs index 36c9335..348f90d 100644 --- a/crates/origin-chain-fees/src/lib.rs +++ b/crates/origin-chain-fees/src/lib.rs @@ -1,11 +1,11 @@ -//! # pallet-origin-fees — Phase 2.4 scaffold v0.0.20 +//! # pallet-origin-fees — Phase 2.4 real impl v0.0.22 //! //! 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 +//! ## Status canonical: v0.0.22 — Real impl + mock runtime integration tests E2E //! //! Este crate provee: //! - Vendor copy `OitFeeSchedule` trait (Mathi commit `ce4fafb`) ✅ @@ -13,23 +13,23 @@ //! - 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) ✅ +//! - Pallet Config trait canonical ✅ +//! - Storage `AccountFees` con saturating arithmetic ✅ //! - Events canonical `FeeCharged` + `FeeRefunded` + `EntitlementBlocked` ✅ -//! - Errors canonical (PROV-like pattern) ✅ -//! - Scaffold dispatchable `compute_fee_for_action` ✅ +//! - Errors canonical (mirror PROV pattern Phase 2.1) ✅ +//! - **Real `compute_fee_for_action` dispatchable canonical (v0.0.22) ✅** +//! - **Mock runtime + 10 integration tests E2E canonical (v0.0.22, pattern v0.0.18 mirror) ✅** +//! - Pure `query_fee` helper canonical para Runtime API consumer (v0.0.22 ✅, exposed v0.0.23) //! -//! ## Lo que NO funciona aún (v0.0.22 real impl + v0.0.23 Runtime API) +//! ## Lo que NO funciona aún (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) +//! - Runtime API canonical `FeesApi.query_fee_for_action` read-only RPC declaration + runtime impl (v0.0.23) //! -//! ## Roadmap canonical post-v0.0.20 +//! ## Roadmap canonical post-v0.0.22 //! -//! 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 +//! 1. v0.0.20: Scaffold trait-first compile-only canonical ✅ +//! 2. v0.0.21: Wired runtime composition Index 8 canonical ✅ +//! 3. **v0.0.22 (ESTA)**: 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 //! @@ -61,7 +61,7 @@ pub use origin_chain_types::{ChainAction, HardwareTier, SubscriptionTier}; pub mod pallet { use frame_support::pallet_prelude::*; use frame_system::pallet_prelude::*; - use origin_chain_types::ChainAction; + use origin_chain_types::{ChainAction, HardwareTier, SubscriptionTier}; /// Pallet configuration trait canonical. /// @@ -197,57 +197,108 @@ pub mod pallet { FeeOverflow, } - /// Dispatchables canonical (scaffold v0.0.20 — real impl v0.0.22). + /// Dispatchables canonical (real impl v0.0.22). #[pallet::call] impl Pallet { - /// OCAFB-5 canonical: `compute_fee_for_action` scaffold dispatchable. + /// OCAFB-5 canonical: `compute_fee_for_action` real 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. + /// **Real impl v0.0.22**: Pure function call vendor `compute_fee()` + /// + storage update `AccountFees` + event emission canonical. Mathi + /// `OitFeeSchedule` trait consumed CLIENT-SIDE (off-chain) — tx submitter + /// queries Mathi per-user → assembles tx con hw + sub baked → submits. + /// Pattern análogo Phase 2.1 `attest_asset(origin, ValidationAttestation)` + /// (ValidationAttestation pre-assembled by Noesis client-side). /// - /// Flow canonical 5-step per OCAFB-5 (v0.0.22 real): + /// Flow canonical 5-step per OCAFB-5: /// 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 + /// 2. Call vendor `compute_fee(base, hw, sub, action)` canonical (pure fn) + /// 3. Si `fee == u64::MAX` → entitlement blocked → emit event + error + /// 4. Update `AccountFees` con saturating arithmetic /// 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, + base: u64, + hw: HardwareTier, + sub: SubscriptionTier, ) -> DispatchResult { - // Verify origin canonical signed. + // Step 1: ensure origin signed canonical let who = ensure_signed(origin)?; - // SCAFFOLD v0.0.20: emit placeholder event canonical. - // Real impl v0.0.22: integrate Mathi OitFeeSchedule + storage insert. + // Step 2: compute fee canonical via vendor pure fn (OCAFB-5 formula) + let fee = crate::compute_fee(base, hw, sub, action); + + // Step 3: handle entitlement blocked canonical (OCAFB-4 Inmutable #37) + // Note: NO event emission here — FRAME canonical reverts events on Err. + // `EntitlementBlocked` event variant reserved para Phase 2.5 future + // `with_storage_layer` pattern (preserve events on error vía dispatch + // transaction). Caller infers blocked entitlement from `Err(EntitlementLocked)`. + if fee == u64::MAX { + return Err(Error::::EntitlementLocked.into()); + } + + // Step 4: update AccountFees canonical con saturating + let mut history = AccountFees::::get(&who).unwrap_or_default(); + history.total_paid = history.total_paid.saturating_add(fee as u128); + history.action_count = history.action_count.saturating_add(1); + AccountFees::::insert(&who, history); + + // Step 5: emit FeeCharged event canonical (OCAFB-5) Self::deposit_event(Event::FeeCharged { who, action, - fee_charged: 0, // placeholder until v0.0.22 real impl + fee_charged: fee, }); Ok(()) } } + + /// Pure helper canonical para Runtime API v0.0.23 consumer + tests. + /// + /// Read-only query canonical sin storage mutation. Wraps vendor `compute_fee` + /// para namespace consistency canonical Runtime API `FeesApi.query_fee_for_action`. + impl Pallet { + /// Query fee canonical sin charge (read-only, no storage write). + /// + /// Consumed canonical por: + /// - `FeesApi.query_fee_for_action` Runtime API canonical (v0.0.23) + /// - Mock runtime tests v0.0.22 canonical + /// - Client-side pricing preview canonical (Irhis Web + Codex + Trinity) + pub fn query_fee( + action: ChainAction, + base: u64, + hw: HardwareTier, + sub: SubscriptionTier, + ) -> u64 { + crate::compute_fee(base, hw, sub, action) + } + } } extern crate alloc; +// v0.0.22 — Mock runtime canonical + integration tests E2E OCAFB-5 flow. +// Pattern v0.0.18 mirror (Phase 2.1 mock runtime integration tests). +#[cfg(test)] +pub mod mock; + +#[cfg(test)] +mod tests_integration; + #[cfg(test)] mod tests { use super::*; - // Compile-time tests canonical scaffold v0.0.20. - // Mock runtime integration tests canonical = v0.0.22 next slice. + // Compile-time tests canonical pallet. + // Mock runtime integration tests canonical = `tests_integration` module v0.0.22. #[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). + fn pallet_compiles_canonical() { + // Pallet compiles canonical — verified by cargo check. + // Real mock runtime integration tests = `tests_integration` module v0.0.22. } #[test] diff --git a/crates/origin-chain-fees/src/mock.rs b/crates/origin-chain-fees/src/mock.rs new file mode 100644 index 0000000..cc0f72a --- /dev/null +++ b/crates/origin-chain-fees/src/mock.rs @@ -0,0 +1,76 @@ +//! # Mock runtime canonical para integration tests v0.0.22 +//! +//! Provee mock minimal `Test` runtime composition (`System` + `Fees` solo) para +//! E2E tests dispatchable + storage + event assertions per OCAFB-5 compute_fee +//! flow canonical: +//! +//! 1. ensure_signed canonical +//! 2. Vendor compute_fee canonical (3-step formula OCAFB-5) +//! 3. Handle entitlement blocked (u64::MAX → Error::EntitlementLocked + event) +//! 4. AccountFees update canonical saturating arithmetic +//! 5. Emit FeeCharged event canonical +//! +//! Pattern v0.0.18 mirror (Phase 2.1 mock runtime integration tests). Mismo +//! pattern construct_runtime! + derive_impl + new_test_ext + helpers. + +#![cfg(test)] + +use crate as origin_chain_fees; +use frame_support::derive_impl; +use sp_runtime::BuildStorage; + +/// Mock block type canonical para tests. +type Block = frame_system::mocking::MockBlock; + +frame_support::construct_runtime!( + pub enum Test { + System: frame_system, + Fees: origin_chain_fees, + } +); + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig)] +impl frame_system::Config for Test { + type Block = Block; + type AccountId = sp_runtime::AccountId32; + type Lookup = sp_runtime::traits::IdentityLookup; +} + +impl origin_chain_fees::Config for Test { + type RuntimeEvent = RuntimeEvent; +} + +/// Test externalities builder canonical (pattern v0.0.18 mirror). +/// +/// Returns new `sp_io::TestExternalities` con frame_system genesis configurado +/// y block_number set to 1 (events requieren block_number > 0). +pub fn new_test_ext() -> sp_io::TestExternalities { + let mut ext: sp_io::TestExternalities = frame_system::GenesisConfig::::default() + .build_storage() + .expect("genesis canonical builds") + .into(); + ext.execute_with(|| { + System::set_block_number(1); + }); + ext +} + +/// Test origin account canonical (ALICE pattern, [1u8; 32] AccountId32). +pub fn alice_origin() -> RuntimeOrigin { + RuntimeOrigin::signed(sp_runtime::AccountId32::new([1u8; 32])) +} + +/// Test account canonical ALICE (AccountId32 value para storage assertions). +pub fn alice_account() -> sp_runtime::AccountId32 { + sp_runtime::AccountId32::new([1u8; 32]) +} + +/// Test origin account canonical (BOB pattern, [2u8; 32] AccountId32). +pub fn bob_origin() -> RuntimeOrigin { + RuntimeOrigin::signed(sp_runtime::AccountId32::new([2u8; 32])) +} + +/// Test account canonical BOB (AccountId32 value para storage assertions). +pub fn bob_account() -> sp_runtime::AccountId32 { + sp_runtime::AccountId32::new([2u8; 32]) +} diff --git a/crates/origin-chain-fees/src/tests_integration.rs b/crates/origin-chain-fees/src/tests_integration.rs new file mode 100644 index 0000000..895b5d5 --- /dev/null +++ b/crates/origin-chain-fees/src/tests_integration.rs @@ -0,0 +1,303 @@ +//! # Integration tests canonical v0.0.22 — Mock runtime E2E OCAFB-5 flow +//! +//! 10 scenarios canonical compute_fee_for_action flow end-to-end via mock runtime: +//! +//! | Scenario | HW | Sub | Action | Expected outcome | +//! |----------|----|----|--------|------------------| +//! | scenario_1_pi5_write_subsidy_90 | EmbeddedArm | Free | Write | fee=10% base (subsidy 9000 bp) | +//! | scenario_2_ultra_write_full_price | Ultra | Pro | Write | fee=base × 1.5 multiplier | +//! | scenario_3_read_always_free_inmutable_37 | Desktop | Free | Read | fee=0 (subsidy 100%) | +//! | scenario_4_free_compute_blocked_entitlement | Desktop | Free | Compute | Error::EntitlementLocked + event | +//! | scenario_5_pro_compute_unlocked | Desktop | Pro | Compute | fee normal | +//! | scenario_6_ultra_compute_premium | Ultra | Ultra | Compute | fee=base × 2.0 multiplier | +//! | scenario_7_storage_accumulates | Desktop | Pro | Write × 3 | history.total_paid sum + count=3 | +//! | scenario_8_chain_action_ordinal_stable_wire | wire format invariant | n/a | Read=0 + Write=1 + Compute=2 + Transfer=3 | +//! | scenario_9_inmutable_16_anti_clasista_lobo_blanco | Pi5 < Ultra fee non-Read | n/a | compile-detect invariant | +//! | scenario_10_5_step_walkthrough | EmbeddedArm | Free | Transfer | explicit Pre/Post-flight assertions | +//! +//! Total: 10 integration tests canonical E2E. Pattern v0.0.18 mirror. + +#![cfg(test)] + +use crate::mock::{alice_account, alice_origin, bob_account, bob_origin, new_test_ext, Fees, RuntimeEvent, System, Test}; +use crate::{AccountFees, Error, Event}; +use frame_support::{assert_noop, assert_ok}; +use origin_chain_types::{ChainAction, HardwareTier, SubscriptionTier}; + +// ─── Scenario 1: Pi 5 Write subsidy 90% canonical (Inmutable #16) ──── + +#[test] +fn scenario_1_pi5_write_subsidy_90_canonical() { + new_test_ext().execute_with(|| { + let base = 10_000u64; + + assert_ok!(Fees::compute_fee_for_action( + alice_origin(), + ChainAction::Write, + base, + HardwareTier::EmbeddedArm, + SubscriptionTier::Free, + )); + + // OCAFB-5 formula: base 10000 × hw_mult 1000 × sub_mult 10000 / 10000 / 10000 = 1000 + // (subsidy 9000 bp → remaining 1000 bp × Free multiplier 10000 bp) + let history = AccountFees::::get(&alice_account()).expect("storage updated"); + assert_eq!(history.total_paid, 1000u128); // Pi 5 paga 10% del base — Inmutable #16 + assert_eq!(history.action_count, 1); + + System::assert_last_event(RuntimeEvent::Fees(Event::FeeCharged { + who: alice_account(), + action: ChainAction::Write, + fee_charged: 1000, + })); + }); +} + +// ─── Scenario 2: Ultra Write Pro multiplier canonical ─────────────── + +#[test] +fn scenario_2_ultra_write_full_price_canonical() { + new_test_ext().execute_with(|| { + let base = 1_000u64; + + assert_ok!(Fees::compute_fee_for_action( + alice_origin(), + ChainAction::Write, + base, + HardwareTier::Ultra, + SubscriptionTier::Pro, + )); + + // OCAFB-5: base 1000 × hw_mult 10000 (Ultra subsidy=0 → full remaining) × sub_mult 15000 / 10000 / 10000 = 1500 + let history = AccountFees::::get(&alice_account()).expect("storage updated"); + assert_eq!(history.total_paid, 1500u128); + assert_eq!(history.action_count, 1); + }); +} + +// ─── Scenario 3: Read always free canonical (Inmutable #37) ────────── + +#[test] +fn scenario_3_read_always_free_inmutable_37_canonical() { + new_test_ext().execute_with(|| { + assert_ok!(Fees::compute_fee_for_action( + alice_origin(), + ChainAction::Read, + 999_999, + HardwareTier::Desktop, // even Desktop + SubscriptionTier::Free, + )); + + // OCAFB-3: Read always 100% subsidy → fee=0 (Inmutable #37 baseline) + let history = AccountFees::::get(&alice_account()).expect("storage updated"); + assert_eq!(history.total_paid, 0u128); + assert_eq!(history.action_count, 1); // count incremented even si fee=0 + }); +} + +// ─── Scenario 4: Free Compute blocked entitlement canonical (Inmutable #37) ── + +#[test] +fn scenario_4_free_compute_blocked_entitlement_canonical() { + new_test_ext().execute_with(|| { + // OCAFB-4 entitlement enforce: Free + Compute → blocked + // FRAME canonical: dispatchable returns `Err(EntitlementLocked)` SIN event + // emission (events revierten on Err per FRAME dispatch transaction). + // `EntitlementBlocked` event variant reserved para Phase 2.5 future + // `with_storage_layer` pattern. + assert_noop!( + Fees::compute_fee_for_action( + alice_origin(), + ChainAction::Compute, + 100, + HardwareTier::Desktop, + SubscriptionTier::Free, // Compute blocked per OCAFB-4 + ), + Error::::EntitlementLocked + ); + + // Storage NOT updated (entitlement blocked early) + assert!(AccountFees::::get(&alice_account()).is_none()); + + // Caller infers blocked entitlement from `Err(EntitlementLocked)` return, + // no event canonical (FRAME revierte events on Err scaffold v0.0.22). + }); +} + +// ─── Scenario 5: Pro Compute unlocked canonical ───────────────────── + +#[test] +fn scenario_5_pro_compute_unlocked_canonical() { + new_test_ext().execute_with(|| { + let base = 100u64; + + assert_ok!(Fees::compute_fee_for_action( + alice_origin(), + ChainAction::Compute, + base, + HardwareTier::Desktop, + SubscriptionTier::Pro, + )); + + // OCAFB-5: base 100 × hw_mult 7500 (Desktop Compute subsidy 2500 → 7500 remaining) × sub_mult 15000 / 10000 / 10000 = 112 + let history = AccountFees::::get(&alice_account()).expect("storage updated"); + assert_eq!(history.total_paid, 112u128); // floor division round-down (112.5 → 112) + assert_eq!(history.action_count, 1); + }); +} + +// ─── Scenario 6: Ultra Compute premium canonical (Inmutable #18) ──── + +#[test] +fn scenario_6_ultra_compute_premium_canonical() { + new_test_ext().execute_with(|| { + let base = 1_000u64; + + assert_ok!(Fees::compute_fee_for_action( + alice_origin(), + ChainAction::Compute, + base, + HardwareTier::Ultra, + SubscriptionTier::Ultra, // EEG Compute Ultra-only (Inmutable #18) + )); + + // OCAFB-5: base 1000 × hw_mult 10000 (Ultra subsidy=0) × sub_mult 20000 / 10000 / 10000 = 2000 + let history = AccountFees::::get(&alice_account()).expect("storage updated"); + assert_eq!(history.total_paid, 2000u128); + assert_eq!(history.action_count, 1); + }); +} + +// ─── Scenario 7: Storage accumulates over multiple writes canonical ── + +#[test] +fn scenario_7_storage_accumulates_canonical() { + new_test_ext().execute_with(|| { + let base = 1_000u64; + + // 3 writes secuenciales + for _ in 0..3 { + assert_ok!(Fees::compute_fee_for_action( + alice_origin(), + ChainAction::Write, + base, + HardwareTier::Desktop, + SubscriptionTier::Pro, + )); + } + + // OCAFB-5: base 1000 × hw_mult 7500 × sub_mult 15000 / 10000 / 10000 = 1125 per write + // After 3 writes: total_paid = 3 × 1125 = 3375, action_count = 3 + let history = AccountFees::::get(&alice_account()).expect("storage updated"); + assert_eq!(history.total_paid, 3375u128); + assert_eq!(history.action_count, 3); + }); +} + +// ─── Scenario 8: ChainAction ordinal stable wire format canonical ──── + +#[test] +fn scenario_8_chain_action_ordinal_stable_wire_canonical() { + // Wire format invariant — sin dependency mock runtime + assert_eq!(ChainAction::Read.ordinal(), 0); + assert_eq!(ChainAction::Write.ordinal(), 1); + assert_eq!(ChainAction::Compute.ordinal(), 2); + assert_eq!(ChainAction::Transfer.ordinal(), 3); + + // Roundtrip canonical + for action in [ChainAction::Read, ChainAction::Write, ChainAction::Compute, ChainAction::Transfer] { + assert_eq!(ChainAction::from_ordinal(action.ordinal()), Some(action)); + } +} + +// ─── Scenario 9: Inmutable #16 anti-clasista lobo blanco canonical ─── + +#[test] +fn scenario_9_inmutable_16_anti_clasista_lobo_blanco_canonical() { + new_test_ext().execute_with(|| { + let base = 10_000u64; + + // Pi 5 user (EmbeddedArm + Free) + assert_ok!(Fees::compute_fee_for_action( + alice_origin(), + ChainAction::Write, + base, + HardwareTier::EmbeddedArm, + SubscriptionTier::Free, + )); + let pi5_fee = AccountFees::::get(&alice_account()).unwrap().total_paid; + + // Ultra user (Ultra + Ultra) + assert_ok!(Fees::compute_fee_for_action( + bob_origin(), + ChainAction::Write, + base, + HardwareTier::Ultra, + SubscriptionTier::Ultra, + )); + let ultra_fee = AccountFees::::get(&bob_account()).unwrap().total_paid; + + // Lobo blanco compile-detect canonical: Pi 5 paga ESTRICTAMENTE MENOS que Ultra + assert!( + pi5_fee < ultra_fee, + "Inmutable #16 anti-clasista violated: Pi 5 fee {pi5_fee} debe ser < Ultra fee {ultra_fee}" + ); + + // Ratio canonical aprox 1:20 (Pi 5 1000 vs Ultra 20000) + // Pi 5: 10000 × 1000 × 10000 / 10000 / 10000 = 1000 + // Ultra: 10000 × 10000 × 20000 / 10000 / 10000 = 20000 + assert_eq!(pi5_fee, 1000u128); + assert_eq!(ultra_fee, 20000u128); + }); +} + +// ─── Scenario 10: 5-step OCAFB-5 walkthrough explicit canonical ────── + +#[test] +fn scenario_10_5_step_walkthrough_canonical() { + new_test_ext().execute_with(|| { + let base = 1_000u64; + let action = ChainAction::Transfer; + let hw = HardwareTier::EmbeddedArm; + let sub = SubscriptionTier::Free; + + // Pre-flight assertions canonical + assert!( + AccountFees::::get(&alice_account()).is_none(), + "PRE-flight OCAFB-2: storage canonical empty" + ); + + // Step 1: ensure_signed canonical (implicit via dispatchable) + // Step 2: vendor compute_fee canonical OCAFB-5 (pure fn call) + // Step 3: entitlement check (Transfer Free allowed, no block) + // Step 4: AccountFees update saturating canonical + // Step 5: emit FeeCharged event canonical + assert_ok!(Fees::compute_fee_for_action( + alice_origin(), + action, + base, + hw, + sub, + )); + + // Post-flight assertions canonical + let history = AccountFees::::get(&alice_account()) + .expect("POST-flight OCAFB-2: AccountFees persisted canonical"); + + // OCAFB-5: base 1000 × hw_mult 1000 (EmbeddedArm Transfer subsidy 9000 → 1000 remaining) × sub_mult 10000 / 10000 / 10000 = 100 + assert_eq!( + history.total_paid, 100u128, + "POST-flight: total_paid canonical Pi 5 anti-clasista" + ); + assert_eq!( + history.action_count, 1, + "POST-flight: action_count canonical incremented" + ); + + System::assert_last_event(RuntimeEvent::Fees(Event::FeeCharged { + who: alice_account(), + action, + fee_charged: 100, + })); + }); +} diff --git a/crates/origin-chain-types/src/tiers.rs b/crates/origin-chain-types/src/tiers.rs index 2bf3b7a..351fc9b 100644 --- a/crates/origin-chain-types/src/tiers.rs +++ b/crates/origin-chain-types/src/tiers.rs @@ -17,6 +17,9 @@ #[derive( Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize, )] +#[derive(parity_scale_codec::Encode, parity_scale_codec::Decode)] +#[derive(parity_scale_codec::DecodeWithMemTracking)] +#[derive(parity_scale_codec::MaxEncodedLen, scale_info::TypeInfo)] pub enum HardwareTier { /// Raspberry Pi 5, mobile phones, low-end ARM — multiplier 0.1×. Hero gated out. EmbeddedArm, @@ -53,6 +56,9 @@ impl HardwareTier { #[derive( Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize, )] +#[derive(parity_scale_codec::Encode, parity_scale_codec::Decode)] +#[derive(parity_scale_codec::DecodeWithMemTracking)] +#[derive(parity_scale_codec::MaxEncodedLen, scale_info::TypeInfo)] pub enum AssetQualityTier { /// Standard quality — available to all hardware tiers. No surcharge. Standard, @@ -89,6 +95,9 @@ impl AssetQualityTier { #[derive( Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize, )] +#[derive(parity_scale_codec::Encode, parity_scale_codec::Decode)] +#[derive(parity_scale_codec::DecodeWithMemTracking)] +#[derive(parity_scale_codec::MaxEncodedLen, scale_info::TypeInfo)] pub enum SubscriptionTier { /// Free tier perpetua (Inmutable #37). Read ALL + basic Write + basic Transfer /// (≤10 OGC/day). Compute blocked — upgrade required.