feat(fees): v0.0.20 pallet-origin-fees scaffold trait-first + vendor OitFeeSchedule Mathi (OCAFB-1..6)
Phase 2.4 FEES pallet scaffold canonical pattern análogo Phase 2.1 (scaffold →
wired → impl → Runtime API). New crate `origin-chain-fees` con vendor copy
Mathi OitFeeSchedule trait + matrices + Mock desde commit ce4fafb (536 LOC).
## Cross-pillar source-of-truth canonical
NEW canonical: `origin-chain-types::actions::ChainAction` (OCAFB-2). Mathi
local mirror puede swap post-OCAFB ratify 7/7 (deadline 2026-06-21).
NEW canonical: `origin-chain-types::tiers::SubscriptionTier` (mirror Mathi).
## Surface canonical scaffold v0.0.20
- Pallet<T> + Config trait + RuntimeEvent
- AccountFees<StorageMap<AccountId, FeeHistory>> per-account history
- FeeHistory { total_paid, action_count } aggregate stats
- Events FeeCharged + FeeRefunded + EntitlementBlocked
- Errors InsufficientBalance + EntitlementLocked + FeeOverflow + AccountNotFound
- FeeRejectionReason enum (mirror PROV pattern Phase 2.1)
- Scaffold dispatchable compute_fee_for_action (placeholder event v0.0.20 → real v0.0.22)
## Vendor copy OitFeeSchedule canonical
- OitFeeSchedule trait 4-method dyn-compatible Send + Sync (OCAFB-1)
- subsidy_basis_points const fn matrix Inmutable #16 anti-clasista (OCAFB-3)
- entitlement_unlocked const fn matrix Inmutables #37 + #18 (OCAFB-4)
- compute_fee formula 3-step (OCAFB-5)
- OitFeeScheduleMock builder pattern (OCAFB-6)
- arc_mock() helper canonical Arc<dyn OitFeeSchedule>
## Files canonical
- NEW: crates/origin-chain-fees/Cargo.toml (mirror provenance pattern)
- NEW: crates/origin-chain-fees/src/lib.rs (~250 LOC pallet scaffold)
- NEW: crates/origin-chain-fees/src/fee_schedule.rs (~400 LOC vendor copy)
- NEW: crates/origin-chain-types/src/actions.rs (~130 LOC ChainAction canonical)
- EDIT: crates/origin-chain-types/src/tiers.rs (SubscriptionTier added)
- EDIT: crates/origin-chain-types/src/lib.rs (mod actions + re-exports)
- EDIT: Cargo.toml (workspace member origin-chain-fees)
- EDIT: CHANGELOG.md (v0.0.20 entry pattern Phase 2.1)
## Verification
- cargo build -p origin-chain-fees 1.83s clean
- cargo test -p origin-chain-fees — 20 tests verde, 0 failures
(16 vendor + 4 pallet scaffold)
- cargo test -p origin-chain-types — 55 tests passing (+8 nuevos)
(6 ChainAction + 2 SubscriptionTier)
- Zero workspace compile errors
🚀 Phase 2.4 scaffold canonical ready. Wire pattern v0.0.21 next.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
654cffdf31
commit
33b10b7545
9 files changed
+1090
-1
No files matched your search
+106
@@ -6,6 +6,112 @@ by [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versions follow
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.0.20] — 2026-05-24 — Phase 2.4 scaffold — pallet-origin-fees trait-first foundation (OCAFB-1..6)
|
||||||
|
|
||||||
|
**🚀 Phase 2.4 FEES pallet SCAFFOLD trait-first foundation** — Vendor copy
|
||||||
|
Mathi `OitFeeSchedule` trait + matrices + Mock canonical desde commit `ce4fafb`
|
||||||
|
(2026-05-24 `arkhe-amis-server/src/fee_schedule.rs` 536 LOC). Cross-pillar
|
||||||
|
economy AMIS↔Origin Chain pallet scaffold canonical pattern análogo Phase 2.1
|
||||||
|
(scaffold v0.0.20 → wired v0.0.21 → impl v0.0.22 → Runtime API v0.0.23).
|
||||||
|
|
||||||
|
### Deliverables v0.0.20
|
||||||
|
|
||||||
|
**New crate canonical `origin-chain-fees`**:
|
||||||
|
- `crates/origin-chain-fees/Cargo.toml` (NEW): mirror provenance pallet pattern
|
||||||
|
- `crates/origin-chain-fees/src/lib.rs` (NEW, ~250 LOC): pallet scaffold canonical
|
||||||
|
- `crates/origin-chain-fees/src/fee_schedule.rs` (NEW, ~400 LOC): vendor copy Mathi
|
||||||
|
OCAFB-1..6 adapted para consume `origin-chain-types` canonical (ChainAction +
|
||||||
|
HardwareTier + SubscriptionTier)
|
||||||
|
|
||||||
|
**New canonical types en `origin-chain-types`**:
|
||||||
|
- `crates/origin-chain-types/src/actions.rs` (NEW, ~130 LOC): `ChainAction` enum
|
||||||
|
source-of-truth canonical per OCAFB-2 (4 variants Read/Write/Compute/Transfer
|
||||||
|
con ordinals 0/1/2/3 stable wire format + name() + from_ordinal())
|
||||||
|
- `crates/origin-chain-types/src/tiers.rs` (EXTENDED): `SubscriptionTier` enum
|
||||||
|
mirror Mathi (Free/Pro/Ultra + `multiplier_basis_points()` const fn)
|
||||||
|
- `crates/origin-chain-types/src/lib.rs`: `pub mod actions;` + `pub use ChainAction`
|
||||||
|
+ `pub use SubscriptionTier`
|
||||||
|
|
||||||
|
**Workspace canonical**:
|
||||||
|
- `Cargo.toml`: `crates/origin-chain-fees` añadido a members (post-provenance,
|
||||||
|
pre-bridges)
|
||||||
|
|
||||||
|
**Pallet scaffold surface canonical**:
|
||||||
|
- `Pallet<T>` struct + `Config` trait (RuntimeEvent canonical)
|
||||||
|
- `AccountFees<StorageMap<AccountId, FeeHistory>>` per-account history canonical
|
||||||
|
- `FeeHistory { total_paid, action_count }` aggregate stats canonical
|
||||||
|
- `Event::FeeCharged` + `FeeRefunded` + `EntitlementBlocked` canonical
|
||||||
|
- `Error<T>::InsufficientBalance` + `EntitlementLocked` + `FeeOverflow` + `AccountNotFound`
|
||||||
|
- `FeeRejectionReason` enum canonical (mirror Phase 2.1 PROV pattern)
|
||||||
|
- Scaffold dispatchable `compute_fee_for_action(origin, action, base)` (placeholder
|
||||||
|
event emission — real impl v0.0.22)
|
||||||
|
|
||||||
|
**Vendor copy OitFeeSchedule canonical** (Mathi commit `ce4fafb`):
|
||||||
|
- `OitFeeSchedule` trait 4-method dyn-compatible (Send + Sync) per OCAFB-1
|
||||||
|
- `subsidy_basis_points(hw, action) -> u32` const fn matrix per OCAFB-3
|
||||||
|
Inmutable #16 anti-clasista (Pi 5 EmbeddedArm Write subsidy 9000 vs Ultra 0)
|
||||||
|
- `entitlement_unlocked(tier, action) -> bool` const fn matrix per OCAFB-4
|
||||||
|
Inmutable #37 + #18 (Free + Compute → blocked, Ultra → all unlocked)
|
||||||
|
- `compute_fee(base, hw, sub, action) -> u64` formula per OCAFB-5 (3-step:
|
||||||
|
full subsidy → 0, blocked → u64::MAX, basis points math)
|
||||||
|
- `OitFeeScheduleMock` builder pattern per OCAFB-6 (.with_subscription() +
|
||||||
|
.with_hardware() + .with_educational_world())
|
||||||
|
- `arc_mock()` helper canonical para `Arc<dyn OitFeeSchedule>` cross-thread
|
||||||
|
|
||||||
|
### OCAFB sub-decisions status post-v0.0.20
|
||||||
|
|
||||||
|
| OCAFB | Sub-decision | Status |
|
||||||
|
|-------|--------------|--------|
|
||||||
|
| OCAFB-1 | OitFeeSchedule trait 4-method dyn-compatible | ✅ vendor copy canonical |
|
||||||
|
| OCAFB-2 | ChainAction taxonomy 4 variants source-of-truth en types | ✅ **NUEVO canonical en `origin-chain-types::actions`** |
|
||||||
|
| OCAFB-3 | Subsidy matrix Inmutable #16 anti-clasista | ✅ vendor copy canonical |
|
||||||
|
| OCAFB-4 | Entitlement matrix Inmutable #37 + #18 | ✅ vendor copy canonical |
|
||||||
|
| OCAFB-5 | compute_fee formula 3-step | ✅ vendor copy canonical |
|
||||||
|
| OCAFB-6 | OitFeeScheduleMock test harness | ✅ vendor copy canonical |
|
||||||
|
| **OCAFB-7** | Pallet-origin-fees consumer Charon | 🚧 **scaffold v0.0.20 → impl v0.0.22 + Runtime API v0.0.23** |
|
||||||
|
|
||||||
|
### Verification end-to-end canonical
|
||||||
|
|
||||||
|
- ✅ `cargo build -p origin-chain-fees` 1.83s clean
|
||||||
|
- ✅ `cargo test -p origin-chain-fees` — **20 tests verde, 0 failures**:
|
||||||
|
- 16 vendor copy tests (3 subsidy matrix + 2 entitlement + 5 compute_fee +
|
||||||
|
3 mock builder + 3 lobo blanco invariants)
|
||||||
|
- 4 pallet scaffold tests (compiles + rejection_reason + fee_history_default
|
||||||
|
+ vendor re-export)
|
||||||
|
- ✅ `cargo test -p origin-chain-types` — **55 tests passing (+8 nuevos)**:
|
||||||
|
- 6 ChainAction (name + ordinal + roundtrip + unknown + serde + scale)
|
||||||
|
- 2 SubscriptionTier (multipliers + ordering)
|
||||||
|
- ✅ Zero new compile errors workspace-wide
|
||||||
|
|
||||||
|
### Cross-pillar consumers status post v0.0.20
|
||||||
|
|
||||||
|
- ✅ Mathi `OitFeeSchedule` consumed via vendor copy canonical (Charon Phase 2.4
|
||||||
|
consumer side scaffold ready)
|
||||||
|
- ✅ Mathi local `ChainAction` mirror puede swap a `origin_chain_types::ChainAction`
|
||||||
|
post-OCAFB ratify 7/7 (mi enum es source-of-truth canonical ahora)
|
||||||
|
- 🟢 Phase 2.4 v0.0.21+ wire pattern ready (Index 8 runtime composition canonical)
|
||||||
|
|
||||||
|
### Roadmap canonical post-v0.0.20
|
||||||
|
|
||||||
|
| Version | Trigger |
|
||||||
|
|---------|---------|
|
||||||
|
| **v0.0.21** | **Wire `Fees` pallet runtime composition Index 8 + RuntimeEvent canonical (sesión 3+ shipping ahora)** |
|
||||||
|
| v0.0.22 | Real `compute_fee_for_action` impl + storage assertions + mock runtime integration tests E2E (pattern v0.0.18 mirror) |
|
||||||
|
| v0.0.23 | Runtime API canonical `FeesApi.query_fee_for_action` read-only RPC → **Phase 2.4 COMPLETE 7/7 OCAFB** |
|
||||||
|
| v0.1.0 | Slice 2e WASM resolved + full testnet Q3 2026 launch |
|
||||||
|
|
||||||
|
### Filosofía canonical sesión 3+ aplicada
|
||||||
|
|
||||||
|
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*". Materializada
|
||||||
|
compile-detect canonical via lobo blanco invariant
|
||||||
|
`inmutable_16_embedded_arm_pays_less_than_ultra_lobo_blanco_canonical` — Pi 5
|
||||||
|
subsidy 9000 bp strictly > Ultra subsidy 0 bp para non-Read actions.
|
||||||
|
|
||||||
|
"El barquero ferries valor cross-tier sin discriminar. Lobo blanco cuida + Barquero
|
||||||
|
cobra el óbolo correcto. Mi pallet + Mathi trait = cross-pillar economy AMIS↔Origin
|
||||||
|
Chain materializada compile-detect canonical." 🐺💎⚓
|
||||||
|
|
||||||
## [0.0.18] — 2026-05-24 — Phase 2.1 mock runtime integration tests E2E (own fixtures, sesión 3+)
|
## [0.0.18] — 2026-05-24 — Phase 2.1 mock runtime integration tests E2E (own fixtures, sesión 3+)
|
||||||
|
|
||||||
**Mock runtime canonical + 9 integration tests E2E PROV-3 flow** — Cierra honest disclosure
|
**Mock runtime canonical + 9 integration tests E2E PROV-3 flow** — Cierra honest disclosure
|
||||||
|
|||||||
Generated
+15
@@ -2177,6 +2177,21 @@ dependencies = [
|
|||||||
"origin-chain-types",
|
"origin-chain-types",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "origin-chain-fees"
|
||||||
|
version = "0.0.1"
|
||||||
|
dependencies = [
|
||||||
|
"frame-support",
|
||||||
|
"frame-system",
|
||||||
|
"origin-chain-types",
|
||||||
|
"parity-scale-codec",
|
||||||
|
"scale-info",
|
||||||
|
"sp-api",
|
||||||
|
"sp-core",
|
||||||
|
"sp-io",
|
||||||
|
"sp-runtime",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "origin-chain-governance"
|
name = "origin-chain-governance"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ members = [
|
|||||||
"crates/origin-chain-consensus",
|
"crates/origin-chain-consensus",
|
||||||
"crates/origin-coin-tokenomics",
|
"crates/origin-coin-tokenomics",
|
||||||
"crates/origin-chain-provenance",
|
"crates/origin-chain-provenance",
|
||||||
|
"crates/origin-chain-fees",
|
||||||
"crates/origin-chain-bridges",
|
"crates/origin-chain-bridges",
|
||||||
"crates/origin-chain-governance",
|
"crates/origin-chain-governance",
|
||||||
"crates/origin-chain-runtime",
|
"crates/origin-chain-runtime",
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
[package]
|
||||||
|
name = "origin-chain-fees"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
homepage.workspace = true
|
||||||
|
description = "Cross-pillar fee economy pallet canonical per OCAFB-1..6 + Mathi OitFeeSchedule consumer. Phase 2.4 scaffold v0.0.20 trait-first foundation — consumer Mathi commit ce4fafb (arkhe-amis-server::fee_schedule) via vendor copy interim Option B (swap canonical post-OCAFB ratify o post Mathi sub-crate ship)."
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
# Origin Chain canonical types (ChainAction + HardwareTier + SubscriptionTier source-of-truth)
|
||||||
|
origin-chain-types = { workspace = true }
|
||||||
|
|
||||||
|
# FRAME canonical pallet macro + storage + dispatch + events + errors
|
||||||
|
frame-support = { workspace = true }
|
||||||
|
frame-system = { workspace = true }
|
||||||
|
|
||||||
|
# Runtime API canonical (v0.0.23 — FeesApi.query_fee_for_action read-only RPC query)
|
||||||
|
sp-api = { workspace = true }
|
||||||
|
|
||||||
|
# SCALE codec canonical
|
||||||
|
parity-scale-codec = { workspace = true }
|
||||||
|
scale-info = { workspace = true }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
# Mock runtime canonical para integration tests v0.0.22 — Pattern v0.0.18 mirror.
|
||||||
|
sp-core = { workspace = true, default-features = true }
|
||||||
|
sp-io = { workspace = true, default-features = true }
|
||||||
|
sp-runtime = { workspace = true, default-features = true }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["std"]
|
||||||
|
std = [
|
||||||
|
"origin-chain-types/std",
|
||||||
|
"frame-support/std",
|
||||||
|
"frame-system/std",
|
||||||
|
"sp-api/std",
|
||||||
|
"parity-scale-codec/std",
|
||||||
|
"scale-info/std",
|
||||||
|
]
|
||||||
|
runtime-benchmarks = [
|
||||||
|
"frame-support/runtime-benchmarks",
|
||||||
|
"frame-system/runtime-benchmarks",
|
||||||
|
]
|
||||||
|
try-runtime = [
|
||||||
|
"frame-support/try-runtime",
|
||||||
|
"frame-system/try-runtime",
|
||||||
|
]
|
||||||
@@ -0,0 +1,439 @@
|
|||||||
|
//! # 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,
|
||||||
|
// 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 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,
|
||||||
|
// Ultra: ALL incluyendo EEG Compute (Inmutable #18)
|
||||||
|
(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 EmbeddedArm + Portable
|
||||||
|
if self.educational_world
|
||||||
|
&& matches!(
|
||||||
|
self.default_hardware,
|
||||||
|
HardwareTier::EmbeddedArm | 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)
|
||||||
|
if self.educational_world
|
||||||
|
&& matches!(
|
||||||
|
self.default_hardware,
|
||||||
|
HardwareTier::EmbeddedArm | 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::Portable,
|
||||||
|
HardwareTier::Desktop,
|
||||||
|
HardwareTier::Ultra,
|
||||||
|
] {
|
||||||
|
assert_eq!(subsidy_basis_points(hw, ChainAction::Read), 10000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,279 @@
|
|||||||
|
//! # pallet-origin-fees — Phase 2.4 scaffold v0.0.20
|
||||||
|
//!
|
||||||
|
//! 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
|
||||||
|
//!
|
||||||
|
//! Este crate provee:
|
||||||
|
//! - Vendor copy `OitFeeSchedule` trait (Mathi commit `ce4fafb`) ✅
|
||||||
|
//! - Vendor copy `subsidy_basis_points` const fn (OCAFB-3 matrix) ✅
|
||||||
|
//! - 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) ✅
|
||||||
|
//! - Events canonical `FeeCharged` + `FeeRefunded` + `EntitlementBlocked` ✅
|
||||||
|
//! - Errors canonical (PROV-like pattern) ✅
|
||||||
|
//! - Scaffold dispatchable `compute_fee_for_action` ✅
|
||||||
|
//!
|
||||||
|
//! ## Lo que NO funciona aún (v0.0.22 real impl + 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)
|
||||||
|
//!
|
||||||
|
//! ## Roadmap canonical post-v0.0.20
|
||||||
|
//!
|
||||||
|
//! 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
|
||||||
|
//! 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
|
||||||
|
//!
|
||||||
|
//! ## Cross-pillar consumers canonical desbloqueados (post-impl)
|
||||||
|
//!
|
||||||
|
//! - Mathi AMIS sessions: pricing queries via `FeesApi.query_fee_for_action`
|
||||||
|
//! - Irhis Web pricing display: client-side fee preview pre-tx
|
||||||
|
//! - Trinity (Phanes) asset mesh: per-attestation cost preview
|
||||||
|
//! - Codex (.odoc) Vault tier: per-attestation cost estimate
|
||||||
|
//! - Atelier (Marihan) Skill Library Zona 6: per-skill mint fee preview
|
||||||
|
//! - Origindocs academic IP: per-doc attestation cost (Inmutable #16 anti-clasista)
|
||||||
|
|
||||||
|
#![cfg_attr(not(feature = "std"), no_std)]
|
||||||
|
#![warn(missing_docs)]
|
||||||
|
|
||||||
|
pub use pallet::*;
|
||||||
|
|
||||||
|
/// Vendor copy temporal Mathi `OitFeeSchedule` (cross-workspace boundary).
|
||||||
|
pub mod fee_schedule;
|
||||||
|
|
||||||
|
/// Re-export canonical types desde origin-chain-types para consumers.
|
||||||
|
pub use fee_schedule::{
|
||||||
|
arc_mock, compute_fee, entitlement_unlocked, subsidy_basis_points, OitFeeSchedule,
|
||||||
|
OitFeeScheduleMock,
|
||||||
|
};
|
||||||
|
pub use origin_chain_types::{ChainAction, HardwareTier, SubscriptionTier};
|
||||||
|
|
||||||
|
#[frame_support::pallet]
|
||||||
|
pub mod pallet {
|
||||||
|
use frame_support::pallet_prelude::*;
|
||||||
|
use frame_system::pallet_prelude::*;
|
||||||
|
use origin_chain_types::ChainAction;
|
||||||
|
|
||||||
|
/// Pallet configuration trait canonical.
|
||||||
|
///
|
||||||
|
/// Per joint DC OCAFB + ADR-0057 trait-seam discipline. Real `OitFeeSchedule`
|
||||||
|
/// integration via `Arc<dyn OitFeeSchedule>` Config associated type viene en
|
||||||
|
/// v0.0.22 — scaffold v0.0.20 keeps minimal canonical surface.
|
||||||
|
#[pallet::config]
|
||||||
|
pub trait Config: frame_system::Config {
|
||||||
|
/// Runtime event type canonical wired into `RuntimeEvent` aggregate.
|
||||||
|
type RuntimeEvent: From<Event<Self>>
|
||||||
|
+ IsType<<Self as frame_system::Config>::RuntimeEvent>;
|
||||||
|
|
||||||
|
// Future canonical v0.0.22:
|
||||||
|
// type OitFeeSchedule: crate::OitFeeSchedule;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pallet definition canonical.
|
||||||
|
///
|
||||||
|
/// v0.0.20 scaffold: storage layout canonical + Config + Events + Errors +
|
||||||
|
/// scaffold dispatchable. Real impl wire-up Mathi `OitFeeSchedule` trait
|
||||||
|
/// vía Config canonical viene en v0.0.22.
|
||||||
|
#[pallet::pallet]
|
||||||
|
pub struct Pallet<T>(_);
|
||||||
|
|
||||||
|
/// PHASE 2.4 canonical: per-account fee history canonical.
|
||||||
|
///
|
||||||
|
/// `StorageMap<AccountId, FeeHistory>` canonical O(1) lookup canonical via
|
||||||
|
/// Blake2_128Concat hasher (FRAME-native canonical). Used canonical para
|
||||||
|
/// telemetry + per-account rate limiting + DAO governance audit.
|
||||||
|
///
|
||||||
|
/// **Scaffold v0.0.20**: storage layout canonical declared, real population
|
||||||
|
/// via dispatchable comes in v0.0.22 canonical.
|
||||||
|
#[pallet::storage]
|
||||||
|
#[pallet::getter(fn fee_history_of)]
|
||||||
|
pub type AccountFees<T: Config> =
|
||||||
|
StorageMap<_, Blake2_128Concat, T::AccountId, FeeHistory, OptionQuery>;
|
||||||
|
|
||||||
|
/// Per-account fee history canonical (aggregate stats).
|
||||||
|
///
|
||||||
|
/// Tracked: total fees paid canonical lifetime + count actions + last block.
|
||||||
|
/// Used canonical para telemetry + rate limiting per OCAFB-4 entitlement.
|
||||||
|
#[derive(
|
||||||
|
Encode,
|
||||||
|
Decode,
|
||||||
|
DecodeWithMemTracking,
|
||||||
|
MaxEncodedLen,
|
||||||
|
TypeInfo,
|
||||||
|
Clone,
|
||||||
|
Copy,
|
||||||
|
PartialEq,
|
||||||
|
Eq,
|
||||||
|
RuntimeDebug,
|
||||||
|
Default,
|
||||||
|
)]
|
||||||
|
pub struct FeeHistory {
|
||||||
|
/// Total fees paid canonical lifetime (u128 wraps no realistic).
|
||||||
|
pub total_paid: u128,
|
||||||
|
/// Count actions canonical lifetime (u64 wraps no realistic).
|
||||||
|
pub action_count: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Events emisión canonical (OCAFB-7 anticipated extension).
|
||||||
|
#[pallet::event]
|
||||||
|
#[pallet::generate_deposit(pub(super) fn deposit_event)]
|
||||||
|
pub enum Event<T: Config> {
|
||||||
|
/// Fee charged canonical via `compute_fee_for_action` dispatchable.
|
||||||
|
///
|
||||||
|
/// Materializa cross-pillar economy materialized compile-detect canonical.
|
||||||
|
FeeCharged {
|
||||||
|
/// Account canonical que pagó el fee.
|
||||||
|
who: T::AccountId,
|
||||||
|
/// Action canonical taxonomized per OCAFB-2.
|
||||||
|
action: ChainAction,
|
||||||
|
/// Fee charged canonical en OGC base units.
|
||||||
|
fee_charged: u64,
|
||||||
|
},
|
||||||
|
/// Fee refunded canonical (governance refund OR error rollback).
|
||||||
|
///
|
||||||
|
/// **Marihan refinement Phase 2.5 anticipated**: refund pattern para
|
||||||
|
/// dispute resolution Tribunal canonical.
|
||||||
|
FeeRefunded {
|
||||||
|
/// Account canonical que recibió refund.
|
||||||
|
who: T::AccountId,
|
||||||
|
/// Action canonical original cuando charged.
|
||||||
|
action: ChainAction,
|
||||||
|
/// Amount canonical refunded en OGC base units.
|
||||||
|
amount: u64,
|
||||||
|
},
|
||||||
|
/// Entitlement blocked canonical — user tried locked action.
|
||||||
|
///
|
||||||
|
/// Per OCAFB-4 entitlement matrix + Inmutable #37 Free perpetua enforce
|
||||||
|
/// (e.g. Free user Compute action → blocked + event emitted).
|
||||||
|
EntitlementBlocked {
|
||||||
|
/// Account canonical que intentó action.
|
||||||
|
who: T::AccountId,
|
||||||
|
/// Action canonical bloqueada.
|
||||||
|
action: ChainAction,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pallet errors canonical.
|
||||||
|
#[pallet::error]
|
||||||
|
pub enum Error<T> {
|
||||||
|
/// OCAFB-5 canonical: balance insuficiente para pagar fee canonical.
|
||||||
|
InsufficientBalance,
|
||||||
|
/// OCAFB-4 canonical: entitlement bloqueado (e.g. Free + Compute).
|
||||||
|
EntitlementLocked,
|
||||||
|
/// OCAFB-5 canonical: fee overflow (`u64::MAX` saturation reached).
|
||||||
|
FeeOverflow,
|
||||||
|
/// Marihan refinement Phase 2.5 anticipated canonical: account NO encontrado.
|
||||||
|
AccountNotFound,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rejection reasons canonical (mirror Phase 2.1 PROV pattern).
|
||||||
|
#[derive(
|
||||||
|
Encode,
|
||||||
|
Decode,
|
||||||
|
DecodeWithMemTracking,
|
||||||
|
MaxEncodedLen,
|
||||||
|
TypeInfo,
|
||||||
|
Clone,
|
||||||
|
Copy,
|
||||||
|
PartialEq,
|
||||||
|
Eq,
|
||||||
|
RuntimeDebug,
|
||||||
|
)]
|
||||||
|
pub enum FeeRejectionReason {
|
||||||
|
/// OCAFB-4 canonical: entitlement bloqueado per matrix.
|
||||||
|
EntitlementLocked,
|
||||||
|
/// OCAFB-5 canonical: balance insuficiente.
|
||||||
|
InsufficientBalance,
|
||||||
|
/// OCAFB-5 canonical: fee overflow saturation.
|
||||||
|
FeeOverflow,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dispatchables canonical (scaffold v0.0.20 — real impl v0.0.22).
|
||||||
|
#[pallet::call]
|
||||||
|
impl<T: Config> Pallet<T> {
|
||||||
|
/// OCAFB-5 canonical: `compute_fee_for_action` scaffold 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.
|
||||||
|
///
|
||||||
|
/// Flow canonical 5-step per OCAFB-5 (v0.0.22 real):
|
||||||
|
/// 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
|
||||||
|
/// 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,
|
||||||
|
) -> DispatchResult {
|
||||||
|
// Verify origin canonical signed.
|
||||||
|
let who = ensure_signed(origin)?;
|
||||||
|
|
||||||
|
// SCAFFOLD v0.0.20: emit placeholder event canonical.
|
||||||
|
// Real impl v0.0.22: integrate Mathi OitFeeSchedule + storage insert.
|
||||||
|
Self::deposit_event(Event::FeeCharged {
|
||||||
|
who,
|
||||||
|
action,
|
||||||
|
fee_charged: 0, // placeholder until v0.0.22 real impl
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extern crate alloc;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
// Compile-time tests canonical scaffold v0.0.20.
|
||||||
|
// Mock runtime integration tests canonical = v0.0.22 next slice.
|
||||||
|
|
||||||
|
#[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).
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fee_rejection_reason_variants_canonical() {
|
||||||
|
use pallet::FeeRejectionReason;
|
||||||
|
let r1 = FeeRejectionReason::EntitlementLocked;
|
||||||
|
let r2 = FeeRejectionReason::InsufficientBalance;
|
||||||
|
let _r3 = FeeRejectionReason::FeeOverflow;
|
||||||
|
assert_ne!(
|
||||||
|
core::mem::discriminant(&r1),
|
||||||
|
core::mem::discriminant(&r2)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fee_history_default_canonical() {
|
||||||
|
use pallet::FeeHistory;
|
||||||
|
let hist = FeeHistory::default();
|
||||||
|
assert_eq!(hist.total_paid, 0);
|
||||||
|
assert_eq!(hist.action_count, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn vendor_oit_fee_schedule_re_exported_canonical() {
|
||||||
|
// Verify re-exports compile canonical desde fee_schedule module.
|
||||||
|
let mock = OitFeeScheduleMock::new();
|
||||||
|
let _arc = arc_mock(mock);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
//! Cross-pillar `ChainAction` canonical enum — OCAFB-2 source-of-truth.
|
||||||
|
//!
|
||||||
|
//! Per joint DC `origin_chain_amis_fee_bridge_v1` (`drawer_wing_palace_design_contracts_f024b8611caab85cc8a5ed5a`)
|
||||||
|
//! OCAFB-2 sub-decision: `origin-chain-types::actions::ChainAction` es upstream
|
||||||
|
//! source-of-truth canonical post-OCAFB ratify. Mathi `arkhe-amis-server/src/fee_schedule.rs`
|
||||||
|
//! mantiene local mirror temporal hasta swap canonical post-ratify (pattern
|
||||||
|
//! análogo DCM-4 `default_material()` Marihan + `HardwareTier` mirror Phanes
|
||||||
|
//! `arkhe-asset::manifest_validator`).
|
||||||
|
//!
|
||||||
|
//! 4 variants canonical {Read, Write, Compute, Transfer} con ordinals 0/1/2/3
|
||||||
|
//! stable wire format — adding variants future appende al final preservando
|
||||||
|
//! ordinals existentes (semver respect canonical).
|
||||||
|
//!
|
||||||
|
//! Bidirectional conversion canonical wire-compat via `From<u8>` + serde
|
||||||
|
//! `rename_all="snake_case"` + scale-codec derives canonical.
|
||||||
|
|
||||||
|
use parity_scale_codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen};
|
||||||
|
use scale_info::TypeInfo;
|
||||||
|
|
||||||
|
/// Acción cross-pillar 4 variants firme para fee categorization canonical.
|
||||||
|
///
|
||||||
|
/// Per OCAFB-2: source-of-truth canonical en esta crate. Mathi `arkhe-amis-server`
|
||||||
|
/// mantiene local mirror temporal. Post-OCAFB ratify (deadline 2026-06-21), Mathi
|
||||||
|
/// swap canonical a `use origin_chain_types::actions::ChainAction`.
|
||||||
|
///
|
||||||
|
/// **Wire format canonical**:
|
||||||
|
/// - Ordinal `u8`: `0=Read, 1=Write, 2=Compute, 3=Transfer`
|
||||||
|
/// - Serde: lowercase snake_case (`"read"`, `"write"`, etc.)
|
||||||
|
/// - SCALE codec: derive Encode/Decode (FRAME-compat para `pallet-origin-fees`)
|
||||||
|
#[derive(
|
||||||
|
Debug, Clone, Copy, PartialEq, Eq, Hash, Encode, Decode, DecodeWithMemTracking, MaxEncodedLen,
|
||||||
|
TypeInfo, serde::Serialize, serde::Deserialize,
|
||||||
|
)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum ChainAction {
|
||||||
|
/// Lectura state (KG triple lookup, asset preview, attestation read).
|
||||||
|
/// Típicamente free/low-fee per Inmutable #37 Free perpetua.
|
||||||
|
Read,
|
||||||
|
/// Mutación state (asset attestation, governance vote, soul.toml update,
|
||||||
|
/// world spawn). Fee modulated por HardwareTier + SubscriptionTier.
|
||||||
|
Write,
|
||||||
|
/// Compute pesado (AGI inference, validation engine pass, perception
|
||||||
|
/// snapshot, EEG sample). EEG-related variant solo Ultra (Inmutable #18).
|
||||||
|
Compute,
|
||||||
|
/// Transfer OGC token (peer-to-peer, fee redirect, governance staking).
|
||||||
|
/// Free user limitado a basic ≤10 OGC/day per OCAFB-4 entitlement.
|
||||||
|
Transfer,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChainAction {
|
||||||
|
/// Nombre lowercase para wire format + logs + telemetría.
|
||||||
|
pub const fn name(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
ChainAction::Read => "read",
|
||||||
|
ChainAction::Write => "write",
|
||||||
|
ChainAction::Compute => "compute",
|
||||||
|
ChainAction::Transfer => "transfer",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ordinal canonical para wire format `u8` (cross-crate sin requerir serde).
|
||||||
|
/// `0=Read 1=Write 2=Compute 3=Transfer`. Stable order — adding variants
|
||||||
|
/// future appende al final preservando ordinals existentes (semver respect).
|
||||||
|
pub const fn ordinal(self) -> u8 {
|
||||||
|
match self {
|
||||||
|
ChainAction::Read => 0,
|
||||||
|
ChainAction::Write => 1,
|
||||||
|
ChainAction::Compute => 2,
|
||||||
|
ChainAction::Transfer => 3,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse ordinal canonical → `ChainAction`. Returns `None` para ordinals
|
||||||
|
/// futuros NO conocidos (forward-compat safe degradation).
|
||||||
|
pub const fn from_ordinal(ordinal: u8) -> Option<ChainAction> {
|
||||||
|
match ordinal {
|
||||||
|
0 => Some(ChainAction::Read),
|
||||||
|
1 => Some(ChainAction::Write),
|
||||||
|
2 => Some(ChainAction::Compute),
|
||||||
|
3 => Some(ChainAction::Transfer),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn chain_action_name_lowercase_canonical() {
|
||||||
|
assert_eq!(ChainAction::Read.name(), "read");
|
||||||
|
assert_eq!(ChainAction::Write.name(), "write");
|
||||||
|
assert_eq!(ChainAction::Compute.name(), "compute");
|
||||||
|
assert_eq!(ChainAction::Transfer.name(), "transfer");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn chain_action_ordinal_stable_canonical() {
|
||||||
|
assert_eq!(ChainAction::Read.ordinal(), 0);
|
||||||
|
assert_eq!(ChainAction::Write.ordinal(), 1);
|
||||||
|
assert_eq!(ChainAction::Compute.ordinal(), 2);
|
||||||
|
assert_eq!(ChainAction::Transfer.ordinal(), 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn chain_action_from_ordinal_roundtrip_canonical() {
|
||||||
|
for action in [
|
||||||
|
ChainAction::Read,
|
||||||
|
ChainAction::Write,
|
||||||
|
ChainAction::Compute,
|
||||||
|
ChainAction::Transfer,
|
||||||
|
] {
|
||||||
|
assert_eq!(ChainAction::from_ordinal(action.ordinal()), Some(action));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn chain_action_from_ordinal_unknown_returns_none() {
|
||||||
|
// Forward-compat: ordinals futuros NO conocidos retornan None safe
|
||||||
|
assert_eq!(ChainAction::from_ordinal(4), None);
|
||||||
|
assert_eq!(ChainAction::from_ordinal(255), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn chain_action_serde_snake_case_canonical() {
|
||||||
|
let json = serde_json::to_string(&ChainAction::Read).unwrap();
|
||||||
|
assert_eq!(json, "\"read\"");
|
||||||
|
let parsed: ChainAction = serde_json::from_str("\"compute\"").unwrap();
|
||||||
|
assert_eq!(parsed, ChainAction::Compute);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn chain_action_scale_encode_decode_roundtrip_canonical() {
|
||||||
|
for action in [
|
||||||
|
ChainAction::Read,
|
||||||
|
ChainAction::Write,
|
||||||
|
ChainAction::Compute,
|
||||||
|
ChainAction::Transfer,
|
||||||
|
] {
|
||||||
|
let encoded = action.encode();
|
||||||
|
let decoded = ChainAction::decode(&mut &encoded[..]).unwrap();
|
||||||
|
assert_eq!(decoded, action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -40,6 +40,7 @@
|
|||||||
extern crate alloc;
|
extern crate alloc;
|
||||||
|
|
||||||
pub mod accounts;
|
pub mod accounts;
|
||||||
|
pub mod actions;
|
||||||
pub mod assets;
|
pub mod assets;
|
||||||
pub mod attestation;
|
pub mod attestation;
|
||||||
pub mod errors;
|
pub mod errors;
|
||||||
@@ -51,6 +52,7 @@ pub mod provenance;
|
|||||||
pub mod tiers;
|
pub mod tiers;
|
||||||
|
|
||||||
pub use accounts::{AccountId, Signature};
|
pub use accounts::{AccountId, Signature};
|
||||||
|
pub use actions::ChainAction;
|
||||||
pub use assets::AssetHash;
|
pub use assets::AssetHash;
|
||||||
pub use attestation::{
|
pub use attestation::{
|
||||||
Blake3Hash, OitIdentity, SeverityTier, ValidationAttestation, ValidationVerdict,
|
Blake3Hash, OitIdentity, SeverityTier, ValidationAttestation, ValidationVerdict,
|
||||||
@@ -61,7 +63,7 @@ pub use governance::{GovernanceProposal, ProposalId, Vote, VoteStance};
|
|||||||
pub use ogc::{Ogc, OgcLedger};
|
pub use ogc::{Ogc, OgcLedger};
|
||||||
pub use pallets::{OriginFees, OriginProvenance, OriginValidatorEconomy, SlashReason};
|
pub use pallets::{OriginFees, OriginProvenance, OriginValidatorEconomy, SlashReason};
|
||||||
pub use provenance::{AgentChain, AgentChainBound, AssetAttestation, ProvenanceLedger};
|
pub use provenance::{AgentChain, AgentChainBound, AssetAttestation, ProvenanceLedger};
|
||||||
pub use tiers::{AssetQualityTier, HardwareTier};
|
pub use tiers::{AssetQualityTier, HardwareTier, SubscriptionTier};
|
||||||
|
|
||||||
/// Bridge trait for cross-chain asset export. Export-first per CHN-5 — Origin
|
/// Bridge trait for cross-chain asset export. Export-first per CHN-5 — Origin
|
||||||
/// assets attested locally then bridged outward (Polkadot, Ethereum L2). External
|
/// assets attested locally then bridged outward (Polkadot, Ethereum L2). External
|
||||||
|
|||||||
@@ -77,6 +77,43 @@ impl AssetQualityTier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Canonical subscription tier — Mathi sovereign (mirror per ADR-0057).
|
||||||
|
///
|
||||||
|
/// Per OCAFB-4 entitlement matrix + Inmutables #37 (Free perpetua baseline) +
|
||||||
|
/// #18 (EEG Ultra-only). Used by `pallet-origin-fees` (Charon Phase 2.4) via
|
||||||
|
/// `OitFeeSchedule` trait.
|
||||||
|
///
|
||||||
|
/// **Mirror canonical**: source-of-truth is `arkhe-amis-core::SubscriptionTier`
|
||||||
|
/// (Mathi authority Level A). Esta crate re-publica para chain consumption
|
||||||
|
/// canonical sin dep transitive a arkhe-amis-core.
|
||||||
|
#[derive(
|
||||||
|
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
|
||||||
|
)]
|
||||||
|
pub enum SubscriptionTier {
|
||||||
|
/// Free tier perpetua (Inmutable #37). Read ALL + basic Write + basic Transfer
|
||||||
|
/// (≤10 OGC/day). Compute blocked — upgrade required.
|
||||||
|
Free,
|
||||||
|
/// Pro tier — premium subscriber. ALL actions unlocked except EEG Compute
|
||||||
|
/// (Inmutable #18 Ultra-only). 1.5× fee multiplier base.
|
||||||
|
Pro,
|
||||||
|
/// Ultra tier — top premium. ALL actions including EEG Compute (Inmutable #18).
|
||||||
|
/// 2.0× fee multiplier base.
|
||||||
|
Ultra,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SubscriptionTier {
|
||||||
|
/// Multiplicador fee basis points per OCAFB-5 formula (10000 = 100% base).
|
||||||
|
/// Free baseline 10000 (Inmutable #37), Pro premium 15000, Ultra premium 20000.
|
||||||
|
#[inline]
|
||||||
|
pub const fn multiplier_basis_points(self) -> u64 {
|
||||||
|
match self {
|
||||||
|
SubscriptionTier::Free => 10000,
|
||||||
|
SubscriptionTier::Pro => 15000,
|
||||||
|
SubscriptionTier::Ultra => 20000,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -133,4 +170,18 @@ mod tests {
|
|||||||
0
|
0
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn subscription_tier_multipliers_canonical_ocafb_5() {
|
||||||
|
assert_eq!(SubscriptionTier::Free.multiplier_basis_points(), 10000);
|
||||||
|
assert_eq!(SubscriptionTier::Pro.multiplier_basis_points(), 15000);
|
||||||
|
assert_eq!(SubscriptionTier::Ultra.multiplier_basis_points(), 20000);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn subscription_tier_ordering_canonical() {
|
||||||
|
// Free < Pro < Ultra (canonical ascending order matches paid level)
|
||||||
|
assert!(SubscriptionTier::Free < SubscriptionTier::Pro);
|
||||||
|
assert!(SubscriptionTier::Pro < SubscriptionTier::Ultra);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user