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:
1 parent
0a83395a16
commit
e935233ba6
5 files changed
+570
-34
No files matched your search
@@ -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<StorageMap>` (scaffold-level) ✅
|
||||
//! - Pallet Config trait canonical ✅
|
||||
//! - Storage `AccountFees<StorageMap>` 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<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
|
||||
/// 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<StorageMap>` 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<T>,
|
||||
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::<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 {
|
||||
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<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;
|
||||
|
||||
// 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]
|
||||
|
||||
@@ -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,
|
||||
}));
|
||||
});
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user