feat(fees): v0.0.22 — compute_fee_for_action impl complete + mock runtime integration tests E2E (10 scenarios)

Phase 2.4 FEES pallet REAL IMPL canonical. Dispatchable compute_fee_for_action
wire-up vendor compute_fee() pure fn call + AccountFees<StorageMap> 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<StorageMap> saturating update canonical
5. Emit FeeCharged event canonical

Pure helper Pallet::<T>::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) <noreply@anthropic.com>
This commit is contained in:
davidherranandClaude Opus 4.7 committed 2026-05-24 22:56:50 -05:00
1 parent 0a83395a16
commit e935233ba6
5 files changed
+570 -34

No files matched your search

+97
View File
@@ -6,6 +6,103 @@ by [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versions follow
## [Unreleased] ## [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<StorageMap>` 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<StorageMap>` update saturating arithmetic canonical
5. Emit `FeeCharged` event canonical
- Pure helper `Pallet::<T>::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 ## [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` **🔌 Phase 2.4 FEES pallet WIRED runtime composition canonical** — `origin-chain-fees`
+85 -34
View File
@@ -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 //! Cross-pillar fee economy pallet canonical per OCAFB-1..6 + Mathi
//! `OitFeeSchedule` consumer. Joint DC `origin_chain_amis_fee_bridge_v1` //! `OitFeeSchedule` consumer. Joint DC `origin_chain_amis_fee_bridge_v1`
//! (`drawer_wing_palace_design_contracts_f024b8611caab85cc8a5ed5a`) abierto //! (`drawer_wing_palace_design_contracts_f024b8611caab85cc8a5ed5a`) abierto
//! 2026-05-24 (Mathi+Charon joint, **6/7 firmas** sesión 3+, deadline 2026-06-21). //! 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: //! Este crate provee:
//! - Vendor copy `OitFeeSchedule` trait (Mathi commit `ce4fafb`) ✅ //! - Vendor copy `OitFeeSchedule` trait (Mathi commit `ce4fafb`) ✅
@@ -13,23 +13,23 @@
//! - Vendor copy `entitlement_unlocked` const fn (OCAFB-4 matrix) ✅ //! - Vendor copy `entitlement_unlocked` const fn (OCAFB-4 matrix) ✅
//! - Vendor copy `compute_fee` formula (OCAFB-5) ✅ //! - Vendor copy `compute_fee` formula (OCAFB-5) ✅
//! - Vendor copy `OitFeeScheduleMock` test harness (OCAFB-6) ✅ //! - Vendor copy `OitFeeScheduleMock` test harness (OCAFB-6) ✅
//! - Pallet Config trait canonical (scaffold-level) ✅ //! - Pallet Config trait canonical ✅
//! - Storage `AccountFees<StorageMap>` (scaffold-level) ✅ //! - Storage `AccountFees<StorageMap>` con saturating arithmetic ✅
//! - Events canonical `FeeCharged` + `FeeRefunded` + `EntitlementBlocked` ✅ //! - Events canonical `FeeCharged` + `FeeRefunded` + `EntitlementBlocked` ✅
//! - Errors canonical (PROV-like pattern) ✅ //! - Errors canonical (mirror PROV pattern Phase 2.1) ✅
//! - Scaffold dispatchable `compute_fee_for_action` ✅ //! - **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) //! - Runtime API canonical `FeesApi.query_fee_for_action` read-only RPC declaration + runtime impl (v0.0.23)
//! - 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 //! ## Roadmap canonical post-v0.0.22
//! //!
//! 1. **v0.0.20 (ESTA)**: Scaffold trait-first compile-only canonical //! 1. v0.0.20: Scaffold trait-first compile-only canonical ✅
//! 2. **v0.0.21**: Wired runtime composition Index 8 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 //! 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 //! 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 //! 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 { pub mod pallet {
use frame_support::pallet_prelude::*; use frame_support::pallet_prelude::*;
use frame_system::pallet_prelude::*; use frame_system::pallet_prelude::*;
use origin_chain_types::ChainAction; use origin_chain_types::{ChainAction, HardwareTier, SubscriptionTier};
/// Pallet configuration trait canonical. /// Pallet configuration trait canonical.
/// ///
@@ -197,57 +197,108 @@ pub mod pallet {
FeeOverflow, FeeOverflow,
} }
/// Dispatchables canonical (scaffold v0.0.20 — real impl v0.0.22). /// Dispatchables canonical (real impl v0.0.22).
#[pallet::call] #[pallet::call]
impl<T: Config> Pallet<T> { impl<T: Config> Pallet<T> {
/// 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 /// **Real impl v0.0.22**: Pure function call vendor `compute_fee()`
/// Config canonical wire-up `OitFeeSchedule` trait. Current canonical solo /// + storage update `AccountFees` + event emission canonical. Mathi
/// validates basic state (origin signed) + emite placeholder event canonical. /// `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 /// 1. `ensure_signed(origin)` canonical
/// 2. Get `hw = oit_schedule.current_hardware_tier(user_id)` + `sub = ...` /// 2. Call vendor `compute_fee(base, hw, sub, action)` canonical (pure fn)
/// 3. Call vendor `compute_fee(base, hw, sub, action)` canonical /// 3. Si `fee == u64::MAX` → entitlement blocked → emit event + error
/// 4. Insert fee record en `AccountFees` StorageMap canonical /// 4. Update `AccountFees<StorageMap>` con saturating arithmetic
/// 5. Emit `FeeCharged` event canonical /// 5. Emit `FeeCharged` event canonical
#[pallet::call_index(0)] #[pallet::call_index(0)]
#[pallet::weight(Weight::from_parts(10_000, 0))] #[pallet::weight(Weight::from_parts(10_000, 0))]
pub fn compute_fee_for_action( pub fn compute_fee_for_action(
origin: OriginFor<T>, origin: OriginFor<T>,
action: ChainAction, action: ChainAction,
_base: u64, base: u64,
hw: HardwareTier,
sub: SubscriptionTier,
) -> DispatchResult { ) -> DispatchResult {
// Verify origin canonical signed. // Step 1: ensure origin signed canonical
let who = ensure_signed(origin)?; let who = ensure_signed(origin)?;
// SCAFFOLD v0.0.20: emit placeholder event canonical. // Step 2: compute fee canonical via vendor pure fn (OCAFB-5 formula)
// Real impl v0.0.22: integrate Mathi OitFeeSchedule + storage insert. 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::<T>::EntitlementLocked.into());
}
// Step 4: update AccountFees<StorageMap> canonical con saturating
let mut history = AccountFees::<T>::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::<T>::insert(&who, history);
// Step 5: emit FeeCharged event canonical (OCAFB-5)
Self::deposit_event(Event::FeeCharged { Self::deposit_event(Event::FeeCharged {
who, who,
action, action,
fee_charged: 0, // placeholder until v0.0.22 real impl fee_charged: fee,
}); });
Ok(()) 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<T: Config> Pallet<T> {
/// 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; 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
// Compile-time tests canonical scaffold v0.0.20. // Compile-time tests canonical pallet.
// Mock runtime integration tests canonical = v0.0.22 next slice. // Mock runtime integration tests canonical = `tests_integration` module v0.0.22.
#[test] #[test]
fn pallet_scaffold_compiles_canonical() { fn pallet_compiles_canonical() {
// Scaffold compiles — verified by cargo check. // Pallet compiles canonical — verified by cargo check.
// Real mock runtime integration tests = v0.0.22 (pattern v0.0.18 mirror). // Real mock runtime integration tests = `tests_integration` module v0.0.22.
} }
#[test] #[test]
+76
View File
@@ -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<StorageMap> 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<Test>;
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<Self::AccountId>;
}
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::<Test>::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])
}
@@ -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::<Test>::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::<Test>::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::<Test>::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::<Test>::EntitlementLocked
);
// Storage NOT updated (entitlement blocked early)
assert!(AccountFees::<Test>::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::<Test>::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::<Test>::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::<Test>::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::<Test>::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::<Test>::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::<Test>::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<StorageMap> 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::<Test>::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,
}));
});
}
+9
View File
@@ -17,6 +17,9 @@
#[derive( #[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize, 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 { pub enum HardwareTier {
/// Raspberry Pi 5, mobile phones, low-end ARM — multiplier 0.1×. Hero gated out. /// Raspberry Pi 5, mobile phones, low-end ARM — multiplier 0.1×. Hero gated out.
EmbeddedArm, EmbeddedArm,
@@ -53,6 +56,9 @@ impl HardwareTier {
#[derive( #[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize, 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 { pub enum AssetQualityTier {
/// Standard quality — available to all hardware tiers. No surcharge. /// Standard quality — available to all hardware tiers. No surcharge.
Standard, Standard,
@@ -89,6 +95,9 @@ impl AssetQualityTier {
#[derive( #[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize, 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 { pub enum SubscriptionTier {
/// Free tier perpetua (Inmutable #37). Read ALL + basic Write + basic Transfer /// Free tier perpetua (Inmutable #37). Read ALL + basic Write + basic Transfer
/// (≤10 OGC/day). Compute blocked — upgrade required. /// (≤10 OGC/day). Compute blocked — upgrade required.