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

@@ -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,
}));
});
}