Espejea HardwareTier::EmbeddedAI (Jetson Orin Nano) en el mirror de la cadena, en lockstep con arkhe-core bbac1b2 (Ergon Fase 1 EmbeddedAI, ADR-0072 / DC 2ba235e1). Trampa de la fuente preservada byte-fiel: EmbeddedAI APPEND discriminante 4 (SCALE wire-safe, attestations runtime_tier:u8 viejas válidas) PERO rank de capacidad 1 (entre EmbeddedArm=0 y Portable=2). Por eso Ord es MANUAL por rank() (no derive — un derive pondría EmbeddedAI > Ultra, falso) y el gating Hero es por rank, no por discriminante. - tiers.rs: variant EmbeddedAI (last) + Ord/PartialOrd manual por rank() + rank()/ordinal()/from_ordinal() SSOT (el reverse-map que evita perder el tier-floor al deserializar, #16) + base_multiplier_x100=10 (piso, el estudiante en Jetson no paga más que en Pi 5) + surcharge Hero gated=0 + supports_hero=false. - fee_schedule.rs: 6 arms EmbeddedAI = piso EmbeddedArm en subsidy_basis_points + educational override incluye EmbeddedAI (device de estudiante). - fees.rs / unified.rs: cobertura — compute_fee(EmbeddedAI)==EmbeddedArm + grid de paridad OCAFB-5 extendido a EmbeddedAI. Tests: 21 suites / 261 workspace verde, 0 regresión. SCALE index == ordinal sellado. FIX-4 (verificador attestation) ortogonal — no mapea el enum, intacto. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
501 lines
21 KiB
Rust
501 lines
21 KiB
Rust
//! # Vendor copy temporal: `OitFeeSchedule` (Mathi `arkhe-amis-server` Sprint Mathi-XIII)
|
||
//!
|
||
//! ## ⚠️ TEMPORARY VENDOR COPY ⚠️
|
||
//!
|
||
//! Will be replaced when:
|
||
//! 1. Mathi ships `arkhe-amis-server` como standalone crates.io release, OR
|
||
//! 2. Post-OCAFB ratify 7/7 (deadline 2026-06-21), Mathi confirma swap canonical
|
||
//! cross-workspace via `[workspace.dependencies]` resolution
|
||
//!
|
||
//! Per OCAFB-1..6 sub-decisions del joint DC `origin_chain_amis_fee_bridge_v1`
|
||
//! (`drawer_wing_palace_design_contracts_f024b8611caab85cc8a5ed5a`) abierto
|
||
//! 2026-05-24 (Mathi+Charon joint, 7 signers, ratify deadline 2026-06-21):
|
||
//!
|
||
//! - **OCAFB-1**: `OitFeeSchedule` trait 4-method dyn-compatible (Mathi authority Level A)
|
||
//! - **OCAFB-3**: `subsidy_basis_points(hw, action) -> u32` matrix Inmutable #16 anti-clasista
|
||
//! - **OCAFB-4**: `entitlement_unlocked(tier, action) -> bool` Inmutable #37 + #18 enforcement
|
||
//! - **OCAFB-5**: `compute_fee(base, hw, sub, action) -> u64` 3-step formula canonical
|
||
//! - **OCAFB-6**: `OitFeeScheduleMock` builder pattern + `arc_mock` test consumer harness
|
||
//!
|
||
//! Source ratificada Mathi: commit `ce4fafb` (2026-05-24T10:28Z) en
|
||
//! `crates/arkhe-amis/crates/arkhe-amis-server/src/fee_schedule.rs` (536 LOC).
|
||
//!
|
||
//! **Adaptaciones canonical**:
|
||
//! - `ChainAction`: consumed desde `origin_chain_types::actions::ChainAction` (source-of-truth
|
||
//! per OCAFB-2, NO vendor local — pallet usa el canonical upstream)
|
||
//! - `HardwareTier`: consumed desde `origin_chain_types::tiers::HardwareTier` (mirror Mathi)
|
||
//! - `SubscriptionTier`: consumed desde `origin_chain_types::tiers::SubscriptionTier` (mirror Mathi)
|
||
//!
|
||
//! Cuando swap canonical happens:
|
||
//! - DELETE este module entero canonical
|
||
//! - Add dep: `arkhe-amis-server = { version = "0.X", default-features = false }`
|
||
//! - Update re-exports en `lib.rs` canonical
|
||
//! - Diary canonical Mathi coordination switch
|
||
|
||
extern crate alloc;
|
||
|
||
use alloc::sync::Arc;
|
||
use origin_chain_types::{ChainAction, HardwareTier, SubscriptionTier};
|
||
|
||
// ─── OitFeeSchedule trait (OCAFB-1) ──────────────────────────────────
|
||
|
||
/// Surface fee modulation cross-pillar Pilar 1 AMIS ↔ Pilar 4 Origin Chain.
|
||
///
|
||
/// Per OCAFB-1 sub-decision del joint DC `origin_chain_amis_fee_bridge_v1`:
|
||
/// 4 methods firmes que `pallet-origin-fees` (Charon Phase 2.4) consume via
|
||
/// `Arc<dyn OitFeeSchedule>`.
|
||
///
|
||
/// **`Send + Sync`**: shared cross-thread between Charon's substrate runtime
|
||
/// + Mathi's AMIS query handlers + Irhis Web pricing display thread.
|
||
/// **dyn-compatible**: pallet holds `Arc<dyn OitFeeSchedule>` per ADR-0057
|
||
/// trait-seam-first pattern.
|
||
pub trait OitFeeSchedule: Send + Sync {
|
||
/// Tier suscripción actual del user (OCAFB-4 entitlement basis).
|
||
/// Default impl produce `SubscriptionTier::Free` per Inmutable #37.
|
||
fn current_subscription_tier(&self, user_id: &str) -> SubscriptionTier;
|
||
|
||
/// Tier hardware actual del user (OCAFB-3 subsidy basis).
|
||
/// Default impl produce `HardwareTier::EmbeddedArm` (safe baseline
|
||
/// Pi 5 default per Inmutable #16 anti-clasista).
|
||
fn current_hardware_tier(&self, user_id: &str) -> HardwareTier;
|
||
|
||
/// `true` cuando user califica para subsidy 100% por (HardwareTier ×
|
||
/// ChainAction) per OCAFB-3 matrix + override `educational_world`.
|
||
/// Inmutable #16 strong materializado.
|
||
fn subsidy_eligible(&self, user_id: &str, action: ChainAction) -> bool;
|
||
|
||
/// Multiplicador fee en basis points (10000 = 100%, 1000 = 10%).
|
||
/// Combina HardwareTier subsidy + SubscriptionTier upgrade per OCAFB-5
|
||
/// formula. `pallet-origin-fees` consume con `base_fee.saturating_mul(mult) / 10000`.
|
||
fn tier_multiplier(&self, user_id: &str, action: ChainAction) -> u64;
|
||
}
|
||
|
||
// ─── OCAFB-3 subsidy matrix (HardwareTier × ChainAction) ────────────
|
||
|
||
/// Matrix subsidy basis points firme per OCAFB-3 Inmutable #16 anti-clasista.
|
||
/// Valores: 10000 = 100% subsidy (free), 0 = no subsidy (pay full fee).
|
||
///
|
||
/// David canon ratificado 2026-05-14: *"La libertad económica del Pi 5 user
|
||
/// en Sub-Saharan Africa NO se acota por el Ultra user en San Francisco"*.
|
||
pub const fn subsidy_basis_points(hw: HardwareTier, action: ChainAction) -> u32 {
|
||
use ChainAction::*;
|
||
use HardwareTier::*;
|
||
match (hw, action) {
|
||
// Read action: siempre 100% subsidy (Inmutable #37 baseline)
|
||
(_, Read) => 10000,
|
||
// EmbeddedArm (Pi 5): subsidy alta — anti-clasista from block 0
|
||
(EmbeddedArm, Write) => 9000,
|
||
(EmbeddedArm, Compute) => 10000, // educational compute free
|
||
(EmbeddedArm, Transfer) => 9000,
|
||
// EmbeddedAI (Jetson Orin Nano): MISMO piso EmbeddedArm — Inmutable #16,
|
||
// el estudiante en Jetson no paga más que en Pi 5. Ergon bbac1b2.
|
||
(EmbeddedAI, Write) => 9000,
|
||
(EmbeddedAI, Compute) => 10000, // educational compute free
|
||
(EmbeddedAI, Transfer) => 9000,
|
||
// Portable: subsidy media
|
||
(Portable, Write) => 7500,
|
||
(Portable, Compute) => 5000,
|
||
(Portable, Transfer) => 7500,
|
||
// Desktop: subsidy moderada
|
||
(Desktop, Write) => 2500,
|
||
(Desktop, Compute) => 2500,
|
||
(Desktop, Transfer) => 2500,
|
||
// Ultra: full price (no subsidy)
|
||
(Ultra, Write) => 0,
|
||
(Ultra, Compute) => 0,
|
||
(Ultra, Transfer) => 0,
|
||
// Phase 3 SBS-5 contract actions — anti-clasista mapping interim
|
||
// (Slice 3.1 SCAFFOLD v0.0.37). Mappeo conservativo análogo a Write
|
||
// (DeployContract + UploadContractCode = storage-heavy) y Compute
|
||
// (CallContract = PolkaVM gas execution). Refinement Slice 3.2 cuando
|
||
// Mathi extiende OitFeeSchedule con measurements empíricos gas/deposit
|
||
// reales post-Phase 3 Ship 2 wired runtime Index 11.
|
||
(EmbeddedArm, DeployContract) => 9000, // Pi 5 dev contracts canonical
|
||
(EmbeddedArm, CallContract) => 10000, // educational call free
|
||
(EmbeddedArm, UploadContractCode) => 9000, // Pi 5 storage subsidy
|
||
// EmbeddedAI (Jetson): mismo piso EmbeddedArm en contract actions (#16).
|
||
(EmbeddedAI, DeployContract) => 9000, // Jetson dev contracts = piso Pi 5
|
||
(EmbeddedAI, CallContract) => 10000, // educational call free
|
||
(EmbeddedAI, UploadContractCode) => 9000, // Jetson storage subsidy = piso
|
||
(Portable, DeployContract) => 7500,
|
||
(Portable, CallContract) => 5000,
|
||
(Portable, UploadContractCode) => 7500,
|
||
(Desktop, DeployContract) => 2500,
|
||
(Desktop, CallContract) => 2500,
|
||
(Desktop, UploadContractCode) => 2500,
|
||
(Ultra, DeployContract) => 0,
|
||
(Ultra, CallContract) => 0,
|
||
(Ultra, UploadContractCode) => 0,
|
||
}
|
||
}
|
||
|
||
// ─── OCAFB-4 entitlement matrix (SubscriptionTier × ChainAction) ────
|
||
|
||
/// `true` cuando user con `tier` puede ejecutar `action` per OCAFB-4 entitlement
|
||
/// unlocks. Inmutable #37 Free perpetua baseline + Inmutable #18 EEG Ultra-only.
|
||
pub const fn entitlement_unlocked(tier: SubscriptionTier, action: ChainAction) -> bool {
|
||
use ChainAction::*;
|
||
use SubscriptionTier::*;
|
||
match (tier, action) {
|
||
// Free: Read ALL + basic Write + basic Transfer (10 OGC/day)
|
||
(Free, Read) => true,
|
||
(Free, Write) => true, // basic operations only (caller validates limits)
|
||
(Free, Compute) => false, // blocked — upgrade required
|
||
(Free, Transfer) => true, // basic ≤10 OGC/day
|
||
// Pro: ALL except advanced Compute (Ultra-only)
|
||
(Pro, Read) => true,
|
||
(Pro, Write) => true,
|
||
(Pro, Compute) => true, // basic compute unlocked
|
||
(Pro, Transfer) => true,
|
||
// Phase 3 SBS-5 contract actions Free tier — conservativo interim
|
||
// (Slice 3.1 SCAFFOLD v0.0.37). Free user blocked de smart contract
|
||
// operations por default. Refinement Slice 3.2 con Mathi: educational
|
||
// world override + Pi 5 dev path canonical via subscription upgrade
|
||
// gratuita post-OCAFB ratify Phase 3 maduración.
|
||
(Free, DeployContract) => false, // requires Pro+ initial
|
||
(Free, CallContract) => false, // requires Pro+ initial
|
||
(Free, UploadContractCode) => false, // requires Pro+ initial
|
||
(Pro, DeployContract) => true,
|
||
(Pro, CallContract) => true,
|
||
(Pro, UploadContractCode) => true,
|
||
// Ultra: ALL incluyendo EEG Compute (Inmutable #18) y contracts
|
||
(Ultra, _) => true,
|
||
}
|
||
}
|
||
|
||
// ─── OCAFB-5 fee compute formula ────────────────────────────────────
|
||
|
||
/// Compute fee per OCAFB-5 formula:
|
||
/// 1. Si subsidy 100% → return 0 (Inmutable #16 strong)
|
||
/// 2. Si entitlement blocked → return u64::MAX (Inmutable #37 enforcement)
|
||
/// 3. `base × hw_mult × sub_mult / 10000` (basis points math)
|
||
///
|
||
/// Aligned canonical ORIGIN_BUSINESS_MODEL_V2 D1-D6 ratificada 2026-05-18.
|
||
pub fn compute_fee(
|
||
base: u64,
|
||
hw: HardwareTier,
|
||
sub: SubscriptionTier,
|
||
action: ChainAction,
|
||
) -> u64 {
|
||
// Step 1: subsidy 100% — Inmutable #16 strong
|
||
let subsidy_bp = subsidy_basis_points(hw, action);
|
||
if subsidy_bp >= 10000 {
|
||
return 0;
|
||
}
|
||
// Step 2: entitlement blocked — Inmutable #37 enforcement
|
||
if !entitlement_unlocked(sub, action) {
|
||
return u64::MAX;
|
||
}
|
||
// Step 3: basis points math (consume SubscriptionTier::multiplier_basis_points canonical)
|
||
let hw_mult = 10000u64.saturating_sub(subsidy_bp as u64); // remainder post-subsidy
|
||
let sub_mult = sub.multiplier_basis_points();
|
||
base.saturating_mul(hw_mult)
|
||
.saturating_mul(sub_mult)
|
||
/ 10000
|
||
/ 10000
|
||
}
|
||
|
||
// ─── OitFeeScheduleMock (OCAFB-6 test harness) ─────────────────────
|
||
|
||
/// Mock impl firme para test consumer harness OCAFB-6 — `pallet-origin-fees`
|
||
/// Charon tests consume via `Arc<dyn OitFeeSchedule>` sin necesitar backend real.
|
||
///
|
||
/// Configurable per-user state via builder pattern. Default produce Free +
|
||
/// EmbeddedArm para todos (safe baseline Inmutable #37 + #16).
|
||
#[derive(Debug, Clone)]
|
||
pub struct OitFeeScheduleMock {
|
||
default_subscription: SubscriptionTier,
|
||
default_hardware: HardwareTier,
|
||
educational_world: bool,
|
||
}
|
||
|
||
impl Default for OitFeeScheduleMock {
|
||
fn default() -> Self {
|
||
Self {
|
||
default_subscription: SubscriptionTier::Free,
|
||
default_hardware: HardwareTier::EmbeddedArm,
|
||
educational_world: false,
|
||
}
|
||
}
|
||
}
|
||
|
||
impl OitFeeScheduleMock {
|
||
/// Constructor con defaults baseline (Free + EmbeddedArm + no override).
|
||
pub fn new() -> Self {
|
||
Self::default()
|
||
}
|
||
|
||
/// Override SubscriptionTier default para tests.
|
||
pub fn with_subscription(mut self, tier: SubscriptionTier) -> Self {
|
||
self.default_subscription = tier;
|
||
self
|
||
}
|
||
|
||
/// Override HardwareTier default para tests.
|
||
pub fn with_hardware(mut self, tier: HardwareTier) -> Self {
|
||
self.default_hardware = tier;
|
||
self
|
||
}
|
||
|
||
/// Habilita override `educational_world` → 100% subsidy EmbeddedArm + Portable
|
||
/// (David canon ratificado: *"la niña en escuela rural recibe el mismo Lab
|
||
/// Faraday que un estudiante Stanford Ultra"*).
|
||
pub fn with_educational_world(mut self, enabled: bool) -> Self {
|
||
self.educational_world = enabled;
|
||
self
|
||
}
|
||
}
|
||
|
||
impl OitFeeSchedule for OitFeeScheduleMock {
|
||
fn current_subscription_tier(&self, _user_id: &str) -> SubscriptionTier {
|
||
self.default_subscription
|
||
}
|
||
|
||
fn current_hardware_tier(&self, _user_id: &str) -> HardwareTier {
|
||
self.default_hardware
|
||
}
|
||
|
||
fn subsidy_eligible(&self, _user_id: &str, action: ChainAction) -> bool {
|
||
// OCAFB-3 override: educational_world → 100% subsidy en los tiers floor
|
||
// EmbeddedArm + EmbeddedAI + Portable (Jetson = device de estudiante, #16).
|
||
if self.educational_world
|
||
&& matches!(
|
||
self.default_hardware,
|
||
HardwareTier::EmbeddedArm | HardwareTier::EmbeddedAI | HardwareTier::Portable
|
||
)
|
||
{
|
||
return true;
|
||
}
|
||
subsidy_basis_points(self.default_hardware, action) >= 10000
|
||
}
|
||
|
||
fn tier_multiplier(&self, _user_id: &str, action: ChainAction) -> u64 {
|
||
// Educational override → multiplier 0 (free) en los tiers floor.
|
||
if self.educational_world
|
||
&& matches!(
|
||
self.default_hardware,
|
||
HardwareTier::EmbeddedArm | HardwareTier::EmbeddedAI | HardwareTier::Portable
|
||
)
|
||
{
|
||
return 0;
|
||
}
|
||
// Composite hw + sub multiplier per OCAFB-5 step 3
|
||
let subsidy_bp = subsidy_basis_points(self.default_hardware, action);
|
||
let hw_mult = 10000u64.saturating_sub(subsidy_bp as u64);
|
||
let sub_mult = self.default_subscription.multiplier_basis_points();
|
||
hw_mult.saturating_mul(sub_mult) / 10000
|
||
}
|
||
}
|
||
|
||
/// Helper para Charon consumer pattern — wrap mock en `Arc<dyn OitFeeSchedule>`.
|
||
pub fn arc_mock(mock: OitFeeScheduleMock) -> Arc<dyn OitFeeSchedule> {
|
||
Arc::new(mock)
|
||
}
|
||
|
||
// ─── Tests OCAFB-6 minimum 7 scenarios + 3 lobo blanco ─────────────
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
// ─── OCAFB-3 subsidy matrix ───────────────────────────────────
|
||
|
||
#[test]
|
||
fn ocafb_3_read_always_full_subsidy_canonical() {
|
||
// Inmutable #37 baseline: Read NEVER charged per HardwareTier
|
||
for hw in [
|
||
HardwareTier::EmbeddedArm,
|
||
HardwareTier::EmbeddedAI,
|
||
HardwareTier::Portable,
|
||
HardwareTier::Desktop,
|
||
HardwareTier::Ultra,
|
||
] {
|
||
assert_eq!(subsidy_basis_points(hw, ChainAction::Read), 10000);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn embedded_ai_subsidy_mirrors_embedded_arm_floor_inmutable_16() {
|
||
// El estudiante en Jetson (EmbeddedAI) no paga MÁS que en Pi 5
|
||
// (EmbeddedArm): piso idéntico en TODA acción. Ergon bbac1b2, #16.
|
||
for action in [
|
||
ChainAction::Read,
|
||
ChainAction::Write,
|
||
ChainAction::Compute,
|
||
ChainAction::Transfer,
|
||
ChainAction::DeployContract,
|
||
ChainAction::CallContract,
|
||
ChainAction::UploadContractCode,
|
||
] {
|
||
assert_eq!(
|
||
subsidy_basis_points(HardwareTier::EmbeddedAI, action),
|
||
subsidy_basis_points(HardwareTier::EmbeddedArm, action),
|
||
"EmbeddedAI debe espejar el piso EmbeddedArm en {action:?}"
|
||
);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn ocafb_3_ultra_pays_full_fee_canonical() {
|
||
// Ultra: no subsidy except Read
|
||
assert_eq!(subsidy_basis_points(HardwareTier::Ultra, ChainAction::Write), 0);
|
||
assert_eq!(subsidy_basis_points(HardwareTier::Ultra, ChainAction::Compute), 0);
|
||
assert_eq!(subsidy_basis_points(HardwareTier::Ultra, ChainAction::Transfer), 0);
|
||
}
|
||
|
||
#[test]
|
||
fn ocafb_3_embedded_arm_subsidy_high_inmutable_16_canonical() {
|
||
// Pi 5 user paga ~10% del Ultra fee (anti-clasista)
|
||
assert_eq!(
|
||
subsidy_basis_points(HardwareTier::EmbeddedArm, ChainAction::Write),
|
||
9000
|
||
);
|
||
assert_eq!(
|
||
subsidy_basis_points(HardwareTier::EmbeddedArm, ChainAction::Compute),
|
||
10000
|
||
);
|
||
}
|
||
|
||
// ─── OCAFB-4 entitlement matrix ───────────────────────────────
|
||
|
||
#[test]
|
||
fn ocafb_4_free_compute_blocked_inmutable_37_canonical() {
|
||
// Free Compute action blocked — upgrade required
|
||
assert!(!entitlement_unlocked(SubscriptionTier::Free, ChainAction::Compute));
|
||
// Free Read/Write/Transfer baseline unlocked
|
||
assert!(entitlement_unlocked(SubscriptionTier::Free, ChainAction::Read));
|
||
assert!(entitlement_unlocked(SubscriptionTier::Free, ChainAction::Write));
|
||
assert!(entitlement_unlocked(SubscriptionTier::Free, ChainAction::Transfer));
|
||
}
|
||
|
||
#[test]
|
||
fn ocafb_4_ultra_unlocks_all_inmutable_18_canonical() {
|
||
// Ultra unlocks ALL actions incluyendo EEG Compute
|
||
for action in [
|
||
ChainAction::Read,
|
||
ChainAction::Write,
|
||
ChainAction::Compute,
|
||
ChainAction::Transfer,
|
||
] {
|
||
assert!(entitlement_unlocked(SubscriptionTier::Ultra, action));
|
||
}
|
||
}
|
||
|
||
// ─── OCAFB-5 fee compute formula ──────────────────────────────
|
||
|
||
#[test]
|
||
fn ocafb_5_pi5_educational_world_free_canonical() {
|
||
let mock = OitFeeScheduleMock::new()
|
||
.with_hardware(HardwareTier::EmbeddedArm)
|
||
.with_educational_world(true);
|
||
assert!(mock.subsidy_eligible("alice", ChainAction::Write));
|
||
assert_eq!(mock.tier_multiplier("alice", ChainAction::Write), 0);
|
||
}
|
||
|
||
#[test]
|
||
fn ocafb_5_ultra_pro_compute_full_fee_canonical() {
|
||
let fee = compute_fee(
|
||
1000,
|
||
HardwareTier::Ultra,
|
||
SubscriptionTier::Pro,
|
||
ChainAction::Compute,
|
||
);
|
||
// base 1000 × hw_mult 10000 × sub_mult 15000 / 10000 / 10000 = 1500
|
||
assert_eq!(fee, 1500);
|
||
}
|
||
|
||
#[test]
|
||
fn ocafb_5_free_user_compute_blocked_u64max_canonical() {
|
||
// Free user Compute blocked — return u64::MAX
|
||
let fee = compute_fee(
|
||
100,
|
||
HardwareTier::Desktop,
|
||
SubscriptionTier::Free,
|
||
ChainAction::Compute,
|
||
);
|
||
assert_eq!(fee, u64::MAX);
|
||
}
|
||
|
||
#[test]
|
||
fn ocafb_5_short_circuit_full_subsidy_canonical() {
|
||
// basis_points >= 10000 → fee=0 (Inmutable #16 strong override step 1)
|
||
let fee = compute_fee(
|
||
999999,
|
||
HardwareTier::EmbeddedArm, // Compute → 10000 subsidy
|
||
SubscriptionTier::Ultra, // even Ultra
|
||
ChainAction::Compute,
|
||
);
|
||
assert_eq!(fee, 0);
|
||
}
|
||
|
||
#[test]
|
||
fn ocafb_5_subsidy_override_educational_david_canon_canonical() {
|
||
let mock = OitFeeScheduleMock::new()
|
||
.with_hardware(HardwareTier::EmbeddedArm)
|
||
.with_subscription(SubscriptionTier::Ultra)
|
||
.with_educational_world(true);
|
||
// Aún con Ultra subscription, educational_world override → free
|
||
assert!(mock.subsidy_eligible("rural-student", ChainAction::Write));
|
||
assert!(mock.subsidy_eligible("rural-student", ChainAction::Compute));
|
||
}
|
||
|
||
// ─── OitFeeScheduleMock builder pattern ──────────────────────
|
||
|
||
#[test]
|
||
fn mock_default_returns_free_embedded_arm_canonical() {
|
||
let mock = OitFeeScheduleMock::new();
|
||
assert_eq!(mock.current_subscription_tier("anyone"), SubscriptionTier::Free);
|
||
assert_eq!(mock.current_hardware_tier("anyone"), HardwareTier::EmbeddedArm);
|
||
}
|
||
|
||
#[test]
|
||
fn mock_builder_with_overrides_canonical() {
|
||
let mock = OitFeeScheduleMock::new()
|
||
.with_subscription(SubscriptionTier::Ultra)
|
||
.with_hardware(HardwareTier::Desktop);
|
||
assert_eq!(mock.current_subscription_tier("user"), SubscriptionTier::Ultra);
|
||
assert_eq!(mock.current_hardware_tier("user"), HardwareTier::Desktop);
|
||
}
|
||
|
||
#[test]
|
||
fn arc_mock_dyn_compatible_canonical() {
|
||
let mock = OitFeeScheduleMock::new();
|
||
let arc: Arc<dyn OitFeeSchedule> = arc_mock(mock);
|
||
assert_eq!(arc.current_subscription_tier("anyone"), SubscriptionTier::Free);
|
||
}
|
||
|
||
// ─── Lobo blanco invariants compile-detect canonical ─────────
|
||
|
||
/// Lobo blanco OCAFB-1: `OitFeeSchedule` trait MUST be dyn-compatible
|
||
/// para que `pallet-origin-fees` (Charon Phase 2.4) pueda hold
|
||
/// `Arc<dyn OitFeeSchedule>` cross-thread.
|
||
#[test]
|
||
fn oit_fee_schedule_trait_object_safe_lobo_blanco_canonical() {
|
||
let _schedule: alloc::boxed::Box<dyn OitFeeSchedule> = alloc::boxed::Box::new(OitFeeScheduleMock::new());
|
||
}
|
||
|
||
/// Lobo blanco OCAFB-1: `OitFeeSchedule` bounds `Send + Sync` para
|
||
/// substrate runtime cross-pillar threading.
|
||
#[test]
|
||
fn oit_fee_schedule_send_sync_lobo_blanco_canonical() {
|
||
fn assert_send_sync<T: Send + Sync>() {}
|
||
assert_send_sync::<OitFeeScheduleMock>();
|
||
}
|
||
|
||
/// Lobo blanco OCAFB-3 Inmutable #16: subsidy matrix MUST guarantee
|
||
/// EmbeddedArm pays strictly LESS than Ultra for non-Read actions
|
||
/// (anti-clasista verification compile-detect).
|
||
#[test]
|
||
fn inmutable_16_embedded_arm_pays_less_than_ultra_lobo_blanco_canonical() {
|
||
for action in [ChainAction::Write, ChainAction::Compute, ChainAction::Transfer] {
|
||
let pi5_subsidy = subsidy_basis_points(HardwareTier::EmbeddedArm, action);
|
||
let ultra_subsidy = subsidy_basis_points(HardwareTier::Ultra, action);
|
||
assert!(
|
||
pi5_subsidy > ultra_subsidy,
|
||
"Lobo blanco Inmutable #16 anti-clasista violated for {action:?}: \
|
||
Pi5 subsidy {pi5_subsidy} bp must be > Ultra subsidy {ultra_subsidy} bp"
|
||
);
|
||
}
|
||
}
|
||
}
|