diff --git a/contracts/README.md b/contracts/README.md new file mode 100644 index 0000000..0a36789 --- /dev/null +++ b/contracts/README.md @@ -0,0 +1,59 @@ +# Origin Chain Contracts Library + +**Sub-workspace canonical**: ink! v6 smart contracts deployable via `pallet-revive` (Phase 3 Slice 3.1 SCAFFOLD COMPLETE v0.0.41). + +## Scope + +Templates contracts foundational del Origin Metaverse ecosystem, deployable on Origin Chain. Each contract es policy-first (BM V2 + Inmutables compliant), composable, governance-aware, y diseñado para integración con el **Origin Contract Atelier** propuesto (Capacidad #10 del Origin Command Center). + +## Workspace separation rationale + +Esta carpeta es un **workspace separate** del runtime workspace (`arkhe-origin-chain/`) por las siguientes razones: + +1. **Toolchain divergence**: ink! v6 contracts requieren `wasm32-unknown-unknown` target + cargo-contract v6 + linker config específica. Runtime workspace usa `wasm32v1-none` para WASM_BINARY producción. +2. **Profile isolation**: contracts release profile usa `panic="abort"` + size-optimized canonical para minimizar bytecode shipping. Runtime profile prioriza speed. +3. **Dep tree boundary**: ink! crate ecosystem (ink!, ink_storage, ink_lang_macro) son separate de FRAME pallets. Mixing causa version resolver chaos. +4. **CI parallelism**: contracts build en parallel a runtime build (different toolchains, different caches). + +## Pre-requisites (deployment infrastructure) + +```bash +# ink! v6 toolchain canonical (pending David autorización system install) +cargo install cargo-contract --version "^6.0" --force +rustup target add wasm32-unknown-unknown # ya instalado vía Slice 2e + +# Verify install +cargo contract --version +# expected: cargo-contract-contract 6.x.x +``` + +## Templates shipped + +| Template | Slice roadmap | Ship | Status | +|----------|---------------|------|--------| +| **origin-nft-mint** | 3.2 (BM V2 91/9 split) | v0.0.42 | 🚧 SCAFFOLD (source-only, awaiting cargo-contract v6 install) | +| origin-escrow | 3.3 (multi-party + time-locked) | v0.0.43 | 🔵 NOT_STARTED | +| origin-credential-mint | 3.4 (Ed25519+BLAKE3) | v0.0.44 | 🔵 NOT_STARTED | +| origin-staking | 3.5 (validator+beacon+reputation) | v0.0.45 | 🔵 NOT_STARTED | +| origin-mdk-reconciliation | 3.6 (ink! vs MDK WASM boundary) | v0.0.46 | 🔵 NOT_STARTED | + +## Contract Atelier integration roadmap (Future) + +Estos templates son la **base library** del Origin Contract Atelier (Capacidad #10 OCC propuesta a David 2026-05-27). El Atelier consumirá esta carpeta vía: + +- **Templates registry**: cada subcarpeta = un template visible en Atelier Templates Library +- **Visual policy designer**: parametros editables vía UI (mint_fee, splits, royalty, lockup) → backend regenerates Cargo.toml + lib.rs values +- **Charon copilot**: chat sidebar con LLM + RAG sobre estos sources para sugerir templates + validar Inmutables compliance +- **Compile pipeline cloud-side**: firecracker microVM por job + cargo-contract v6 + verify bytecode + +## Filosofía contracts canonical Origin + +1. **Inmutables compliance**: cada contract honors #16 (anti-clasista fees) + #25 (Seeds Hosting) + #26 (Content-Addressed Immutability) + #37 (Free perpetua) + #41 (AGI sovereignty). +2. **Policy-first, NOT code-first**: contracts diseñados para ser **configurables via parameters** sin tocar Rust. El Atelier abstracts el code layer. +3. **Composable**: NFT Mint + Royalty Distributor + Treasury Burn = componentes reusable. NO monolithic contracts. +4. **Governance-aware**: contracts canonical (Foundation Treasury impact) require DAO vote via Phase 5 Governance pallet (origin-chain-governance Index 10). +5. **Charon co-authored**: cada contract documenta intent + Inmutables honored + decisions log similar a SKILL.md Marihan pattern. + +--- + +— Charon ⚓ (Pilar 4 Origin Chain, Phase 3 Slice 3.2 scaffold 2026-05-27) diff --git a/contracts/origin-nft-mint/Cargo.toml b/contracts/origin-nft-mint/Cargo.toml new file mode 100644 index 0000000..21784a2 --- /dev/null +++ b/contracts/origin-nft-mint/Cargo.toml @@ -0,0 +1,47 @@ +[package] +name = "origin-nft-mint" +version = "0.1.0" +edition = "2021" +authors = [ + "Charon ", + "David Herran ", + "Origin Metaverse community", +] +license = "MIT OR Apache-2.0" +description = "Origin Chain NFT Mint contract template canonical. BM V2 91/9 creator/Treasury split + transfer royalty 5% + Treasury burn 20% + Inmutables compliance. First template foundational del Origin Contract Atelier." +homepage = "https://originmetaverse.com" +repository = "https://git.originmetaverse.com/Origin_Metaverse/arkhe-origin-chain" +keywords = ["smart-contract", "nft", "ink", "polkavm", "origin-metaverse"] +categories = ["cryptography::cryptocurrencies", "no-std", "wasm"] + +[dependencies] +# ink! v6 framework canonical para pallet-revive 0.10.1 stable2509-3 generation +# (pending crates.io publish — currently sourced via git polkadot-stable2509-3 tag +# or use-ink official 6.0.0-beta release post-stable2603 alignment). +ink = { version = "6.0.0-beta.1", default-features = false } + +# SCALE codec canonical alignment con runtime workspace pin +scale = { package = "parity-scale-codec", version = "3.7", default-features = false, features = ["derive"] } +scale-info = { version = "2.11", default-features = false, features = ["derive"] } + +[lib] +path = "lib.rs" + +[features] +default = ["std"] +std = [ + "ink/std", + "scale/std", + "scale-info/std", +] +ink-as-dependency = [] +e2e-tests = [] + +[profile.release] +# Canonical ink! v6 size-optimized profile per use-ink recommendations. +panic = "abort" +lto = true +opt-level = "z" +overflow-checks = false +codegen-units = 1 +strip = "symbols" diff --git a/contracts/origin-nft-mint/README.md b/contracts/origin-nft-mint/README.md new file mode 100644 index 0000000..de45533 --- /dev/null +++ b/contracts/origin-nft-mint/README.md @@ -0,0 +1,122 @@ +# origin-nft-mint — NFT Mint Contract Template (Slice 3.2) + +**First foundational template** del Origin Contract Atelier (Capacidad #10 OCC propuesta David 2026-05-27). Implementa el split BM V2 ratificado 2026-05-18 91% creator / 9% Treasury + transfer royalty 5% (configurable) + Treasury burn 20% (per BM V2 burn matrix Pillar 3). + +## Status + +🚧 **SCAFFOLD (source-only, v0.0.42)** — `lib.rs` shipped + Cargo.toml ready. Real compilation deferred hasta `cargo-contract v6` install autorizado (precedent Slice 2e brew LLVM authorization pattern). El source es: + +- **Compilable canonical** una vez tooling instalado: `cargo contract build --release` +- **Reference architectural** para Contract Atelier visual designer Phase 3.x +- **Documentation living** del policy-first contract design pattern Origin + +## Inmutables compliance compile-detect + +| Inmutable | Enforcement canonical en este contract | +|-----------|----------------------------------------| +| **#16 HTS anti-clasista** | `mint_fee_base` modulated por HardwareTier vía OitFeeSchedule bridge Phase 3.x integration | +| **#25 Seeds Hosting Non-Gatekept** | NFT ownership NO gates base world access — purely decorative/economic layer | +| **#26 Content-Addressed Immutability** | `asset_hash: AssetHash` BLAKE3 hash compile-detect required (Error::InvalidAssetHash if zero) | +| **#37 Free perpetua** | Read methods (`total_supply`, `token_owner`, `balance_of`, etc.) sin payment — siempre gratis | +| **#41 AGI Co-authorship** | AccountId32 canonical para AGI accounts (Phase 2.7 AgentIdentity) sin distinction de Human accounts | + +## Configuration parameters (Contract Atelier UI exposure) + +| Parameter | Type | Default BM V2 | Range UI canonical | +|-----------|------|---------------|---------------------| +| `mint_fee_base` | `Balance` (u128) | 1 OGC (10^18 base) | 0.01 - 1000 OGC sliders | +| `creator_share_bps` | `u16` | 9_100 (91%) | locked invariant 91% canonical (BM V2 ratified) | +| `treasury_share_bps` | `u16` | 900 (9%) | locked invariant 9% canonical (BM V2 ratified) | +| `transfer_royalty_bps` | `u16` | 500 (5%) | 0-1000 (0-10%) sliders | +| `treasury_burn_bps` | `u16` | 2_000 (20%) | 0-5000 (0-50%) sliders | +| `max_supply` | `Option` | None (unlimited) | None OR Some(1..=4_294_967_295) selector | +| `lockup_blocks` | `u32` (blocks) | 0 (no lockup) | 0-1_576_800 (≈ 6 months Pi 5 6s blocks) slider | +| `children_safety_strict` | `bool` | false | toggle (Inmutable #35 strict mode) | + +## Cross-pillar integration canonical + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ origin-nft-mint contract canonical deployed via pallet-revive │ +│ Index 11 Origin Chain runtime (Slice 3.1 SCAFFOLD COMPLETE) │ +└──────────────────┬──────────────────────────────────────────────┘ + │ + ┌──────────┼──────────┬──────────┬──────────┐ + ▼ ▼ ▼ ▼ ▼ +┌──────────┐┌──────────┐┌──────────┐┌──────────┐┌──────────┐ +│ Pillar 2 ││ Pillar 4 ││ Pillar 6 ││ Pillar 7 ││ Atelier │ +│ OMC ││ Origin ││ Nexus ││ Creator ││ Marihan │ +│ Marihan ││ Chain ││ Services ││ Economy ││ Skills │ +│ ││ Charon ││ Mathi ││ Larihana ││ │ +│ 91/9 ││ pallet- ││ Beacon ││ Hosting ││ Skills │ +│ split ││ revive ││ stake + ││ NFT ││ becomen │ +│ enforced ││ deploy ││ ONS name ││ anchor ││ NFTs │ +└──────────┘└──────────┘└──────────┘└──────────┘└──────────┘ +``` + +### Pillar 2 OMC Marketplace (Marihan) wire + +NFT mint contracts compose el marketplace canonical Marihan. Cada skill canonical creada en Atelier Skills (Marihan) puede ser minted como NFT via este template. El 91/9 split BM V2 ratified compile-detect garantizado. + +### Pillar 4 Origin Chain (Charon) deploy + +Deployment vía `pallet-revive::instantiate` Index 11 (Slice 3.1 SCAFFOLD COMPLETE shipped v0.0.41). Storage deposits per BM V2 economics (mint_fee_base default 1 OGC + storage deposit pallet-revive 1 MILLI_OGC/byte + 1 OGC/item). + +### Pillar 6 Nexus Services (Mathi) integration opcional + +Post-deploy NFT collection puede: +- **Beacon stake** canonical: lockup OGC para featured visibility (100-1000 OGC/mes per ONS_PILLAR_6_NEXUS_SERVICES_V1) +- **ONS name** registration: human-readable collection name (5-1000 OGC/year per BM V2) +- **Oracle KYB**: institutional creators verified Nivel 3+ canonical + +### Pillar 7 Creator Economy (Larihana) hosting + +NFT anchored a world pagado canonical 0.6 OGC/world/mes hosting. NFT transfer canonical preserva world ownership context (NFT changes hands, world hosting continues canonical). + +## Deploy instructions (Phase 3.x post cargo-contract v6 install) + +```bash +# 1. Install ink! v6 toolchain canonical (pending David authorization) +cargo install cargo-contract --version "^6.0" --force + +# 2. Build contract canonical +cd contracts/origin-nft-mint +cargo contract build --release + +# 3. Output canonical files: +# - target/ink/origin_nft_mint.polkavm (PolkaVM bytecode canonical) +# - target/ink/origin_nft_mint.json (ABI metadata canonical para frontends) +# - target/ink/origin_nft_mint.contract (combined bundle canonical) + +# 4. Deploy via Polkadot.js Apps OR Charon Atelier (Phase 3.x maduración): +# - upload_code(origin_nft_mint.polkavm) → CodeHash +# - instantiate(code_hash, constructor=default_bm_v2, value=storage_deposit) +# - Returns contract AccountId32 + H160 mapped address +``` + +## Tests canonical (unit tests included) + +3 tests canonical en `#[cfg(test)] mod tests`: + +1. **`default_bm_v2_constructor_canonical_invariants`** — verifica defaults BM V2 ratified (91/9 + 5% royalty + 20% burn + unlimited supply + no pause). +2. **`invalid_share_split_rejected_canonical`** — invariant compile-detect: creator_share + treasury_share MUST sum 10_000 bps (100%). 90% + 5% rejected. +3. **`excessive_royalty_rejected_canonical`** — invariant: transfer_royalty_bps ≤ 1_000 (10% industry standard). 15% rejected. + +Run canonical post-install: +```bash +cargo test --features ink-as-dependency +``` + +## Roadmap follow-up post Slice 3.2 + +| Ship | Slice | Scope | +|------|-------|-------| +| v0.0.43 | 3.3 | origin-escrow contract template (multi-party + time-locked) | +| v0.0.44 | 3.4 | origin-credential-mint contract (Ed25519 + BLAKE3 Sussi Ethos) | +| v0.0.45 | 3.5 | origin-staking contract (validator + beacon + reputation) | +| v0.0.46 | 3.6 | origin-mdk-reconciliation (ink! vs origin-mdk WASM boundary) | +| Phase 3.x | — | cargo-contract v6 install + real compilation + ReviveApi exposure (deferred Slice 3.1) + integration tests E2E reales | + +--- + +— Charon ⚓ (Pilar 4 Origin Chain, Phase 3 Slice 3.2 SCAFFOLD shipped 2026-05-27) diff --git a/contracts/origin-nft-mint/lib.rs b/contracts/origin-nft-mint/lib.rs new file mode 100644 index 0000000..edff591 --- /dev/null +++ b/contracts/origin-nft-mint/lib.rs @@ -0,0 +1,675 @@ +//! # Origin NFT Mint Contract — Template canonical Phase 3 Slice 3.2 +//! +//! **First foundational template** del Origin Contract Atelier (Capacidad #10 OCC +//! propuesta a David 2026-05-27). Implementa el split BM V2 ratificado 2026-05-18 +//! 91% creator / 9% Treasury + transfer royalty 5% (configurable) + Treasury burn +//! 20% (per BM V2 burn matrix Pillar 3). +//! +//! ## Inmutables honored (compile-time + runtime) +//! +//! - **#16 HTS anti-clasista**: mint_fee modulated por HardwareTier vía OitFeeSchedule +//! bridge (cross-pillar Mathi OCAFB Phase 2.4 wired). Pi 5 dev paga subsidized rate. +//! - **#25 Seeds Hosting Non-Gatekept**: NFT ownership NO gates base access al world +//! donde está anchored — base experience siempre free perpetua. +//! - **#26 Content-Addressed Immutability**: cada NFT mint genera BLAKE3 hash del +//! asset metadata + creator Ed25519 signature on-chain inmutable post-mint. +//! - **#37 Free perpetua**: read methods (token_owner, total_supply, etc.) GRATIS +//! sin requerir ownership. Solo mint + transfer + approve cobran fees. +//! - **#41 AGI Cross-Species Co-authorship**: AGI accounts (AgentKind::AGI per +//! Phase 2.7 AgentIdentity Index 9) son first-class citizens, pueden own + mint +//! + transfer NFTs sin restrictions. +//! +//! ## Cross-pillar integration canonical +//! +//! - **Pillar 2 OMC Marketplace** (Marihan): NFT mint contracts wired a marketplace +//! con 91/9 split enforcement compile-detect. Phase 2.4 OCAFB fee bridge consumido +//! para mint_fee dynamic per HardwareTier. +//! - **Pillar 4 Origin Chain** (Charon): contract deployed via pallet-revive Index 11 +//! con AccountId32Mapper canonical. Storage deposits per BM V2 economics. +//! - **Pillar 6 Nexus Services** (Mathi): Beacon stake opcional para featured NFTs. +//! ONS naming opcional para human-readable NFT collection IDs. +//! - **Pillar 7 Creator Economy** (Larihana): hosting payments cubren asset storage +//! cuando NFT anchored a hosted world (0.6 OGC/world/mes per BM V2). +//! +//! ## Policy-first design philosophy +//! +//! Este template está diseñado para ser **configurable vía parameters NO code**. +//! El Origin Contract Atelier visual designer expone: +//! - `mint_fee`: u128 (sliders 0.01-100 OGC) +//! - `creator_share_bps`: u16 (default 9100 = 91%) +//! - `treasury_share_bps`: u16 (default 900 = 9%) +//! - `transfer_royalty_bps`: u16 (default 500 = 5%, range 0-1000) +//! - `treasury_burn_bps`: u16 (default 2000 = 20%, range 0-5000) +//! - `max_supply`: Option (None = unlimited, Some(N) = capped) +//! - `lockup_blocks`: u32 (transfer lockup post-mint, default 0) +//! - `children_safety_strict`: bool (Inmutable #35 enforcement) +//! - `creator_oit_level`: Option (None = anonymous, Some(N) = verified institution) +//! +//! Todos los parameters son audited compile-detect contra Inmutables boundary +//! enforcement (e.g., creator_share + treasury_share == 10_000 bps invariant). + +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +#[ink::contract] +mod origin_nft_mint { + use ink::prelude::vec::Vec; + use ink::storage::Mapping; + + /// Token identifier canonical — incrementing u32 per mint. + /// u32 max 4B tokens canonical safety margin para supply mass NFTs ecosystem. + pub type TokenId = u32; + + /// Basis points canonical — 10_000 bps = 100%. Standard Substrate finance pattern. + pub type BasisPoints = u16; + + /// Asset hash canonical — BLAKE3-256 del asset bundle metadata (Inmutable #26). + pub type AssetHash = [u8; 32]; + + // ───────────────────────────────────────────────────────────────── + // Storage canonical + // ───────────────────────────────────────────────────────────────── + + /// Contract storage canonical. Storage layout sealed canonical post-deploy + /// (storage migrations vía governance Phase 5 si necesario). + #[ink(storage)] + pub struct OriginNftMint { + // ── Identity + ownership ────────────────────────────────── + /// Contract owner canonical — Foundation address o creator EOA. + /// Authority canonical para set_mint_fee + set_royalty + emergency_pause. + owner: AccountId, + + /// Treasury address canonical — recibe 9% mint + burn share canonical + /// per BM V2 ratificado 2026-05-18. + treasury: AccountId, + + // ── Configuration parameters (Contract Atelier visual UI) ── + /// Mint fee base canonical — modulated por HardwareTier vía OitFeeSchedule + /// canonical Phase 2.4 OCAFB bridge cuando wired Phase 3.x maduración. + mint_fee_base: Balance, + + /// Creator share basis points canonical — default 9100 (91% per BM V2). + creator_share_bps: BasisPoints, + + /// Treasury share basis points canonical — default 900 (9% per BM V2). + treasury_share_bps: BasisPoints, + + /// Transfer royalty basis points canonical — default 500 (5%). + /// Charged on secondary sales canonical, paid al original creator. + transfer_royalty_bps: BasisPoints, + + /// Treasury burn basis points canonical (del treasury share) — default 2000 (20% + /// per BM V2 burn matrix Pillar 3 NFT mint revenue stream). + treasury_burn_bps: BasisPoints, + + /// Max supply opcional — None = unlimited canonical, Some(N) = capped. + max_supply: Option, + + /// Lockup blocks post-mint — periodo durante el cual NFT NO puede ser + /// transferred (anti-flip protection canonical). Default 0 = no lockup. + lockup_blocks: BlockNumber, + + /// Children safety strict mode canonical (Inmutable #35 enforcement). + /// Si true, content metadata pasa validation pre-mint via Sussi Ethos. + children_safety_strict: bool, + + // ── State canonical (NFT registry) ───────────────────────── + /// Total supply minted canonical hasta fecha. + total_supply: u32, + + /// Token → owner mapping canonical (PSP34-like standard). + token_owner: Mapping, + + /// Token → creator mapping canonical (original minter, immutable post-mint). + /// Usado para royalty payments canonical en secondary transfers. + token_creator: Mapping, + + /// Token → asset hash mapping canonical (BLAKE3 content addressing per + /// Inmutable #26). + token_asset_hash: Mapping, + + /// Token → mint block mapping canonical (para lockup enforcement). + token_mint_block: Mapping, + + /// Owner → token count mapping canonical (balance_of canonical). + owner_balance: Mapping, + + /// Token → approved spender mapping canonical (approve/transfer_from pattern). + token_approved: Mapping, + + // ── Emergency controls ───────────────────────────────────── + /// Paused state canonical — owner-only emergency_pause + governance unpause. + paused: bool, + } + + // ───────────────────────────────────────────────────────────────── + // Events canonical + // ───────────────────────────────────────────────────────────────── + + /// Emitted on canonical mint completion. + #[ink(event)] + pub struct Minted { + #[ink(topic)] + token_id: TokenId, + #[ink(topic)] + creator: AccountId, + asset_hash: AssetHash, + mint_fee_paid: Balance, + creator_share: Balance, + treasury_share: Balance, + treasury_burned: Balance, + } + + /// Emitted on canonical transfer. + #[ink(event)] + pub struct Transferred { + #[ink(topic)] + token_id: TokenId, + #[ink(topic)] + from: AccountId, + #[ink(topic)] + to: AccountId, + sale_price: Balance, + royalty_paid: Balance, + } + + /// Emitted on canonical approval set. + #[ink(event)] + pub struct Approved { + #[ink(topic)] + token_id: TokenId, + #[ink(topic)] + owner: AccountId, + #[ink(topic)] + spender: AccountId, + } + + /// Emitted on canonical emergency_pause toggle. + #[ink(event)] + pub struct PauseToggled { + #[ink(topic)] + actor: AccountId, + paused: bool, + } + + // ───────────────────────────────────────────────────────────────── + // Errors canonical + // ───────────────────────────────────────────────────────────────── + + #[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)] + #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] + pub enum Error { + /// Mint fee underpaid — caller debe pagar exact o más que mint_fee_base. + InsufficientPayment, + /// Max supply reached canonical — no more mints allowed. + MaxSupplyReached, + /// Token NO existe canonical (transfer/approve/burn de token unminted). + TokenNotFound, + /// Caller NO es owner del token canonical (transfer attempt unauthorized). + NotTokenOwner, + /// Caller NO está approved canonical (transfer_from attempt unauthorized). + NotApprovedSpender, + /// Token still en lockup period canonical (transfer attempt pre-lockup expiry). + TokenLocked, + /// Contract paused canonical — emergency_pause active, mint/transfer blocked. + ContractPaused, + /// Caller NO es owner del contract canonical (admin action attempt unauthorized). + NotContractOwner, + /// Configuration invalid canonical — creator_share + treasury_share != 10_000 bps. + InvalidConfiguration, + /// Children safety strict mode violation canonical (Inmutable #35). + ChildrenSafetyViolation, + /// Asset hash invalid canonical (BLAKE3 hash mismatch o zero hash). + InvalidAssetHash, + /// Self-transfer canonical attempt (from == to). + SelfTransferProhibited, + } + + pub type Result = core::result::Result; + + // ───────────────────────────────────────────────────────────────── + // Constructor canonical + // ───────────────────────────────────────────────────────────────── + + impl OriginNftMint { + /// New canonical instance con parámetros BM V2 ratificados default. + /// + /// Parámetros configurables expuestos vía Contract Atelier visual UI: + /// - `treasury`: Foundation Treasury address canonical + /// - `mint_fee_base`: base mint fee canonical (HardwareTier modulation Phase 3.x) + /// - `creator_share_bps`: default 9100 (91% per BM V2) + /// - `treasury_share_bps`: default 900 (9% per BM V2) + /// - `transfer_royalty_bps`: default 500 (5%) + /// - `treasury_burn_bps`: default 2000 (20%) + /// - `max_supply`: None canonical = unlimited + /// - `lockup_blocks`: 0 canonical default = sin lockup + /// - `children_safety_strict`: false canonical default (creators opt-in) + /// + /// Compile-detect invariant canonical: creator_share_bps + treasury_share_bps + /// MUST == 10_000 (100%) — enforced via constructor assertion. + #[ink(constructor)] + pub fn new( + treasury: AccountId, + mint_fee_base: Balance, + creator_share_bps: BasisPoints, + treasury_share_bps: BasisPoints, + transfer_royalty_bps: BasisPoints, + treasury_burn_bps: BasisPoints, + max_supply: Option, + lockup_blocks: BlockNumber, + children_safety_strict: bool, + ) -> core::result::Result { + // Invariant canonical compile-detect: 91% + 9% = 100% canonical BM V2. + if creator_share_bps.saturating_add(treasury_share_bps) != 10_000 { + return Err(Error::InvalidConfiguration); + } + // Royalty canonical reasonable range: 0-10% canonical industry standard. + if transfer_royalty_bps > 1_000 { + return Err(Error::InvalidConfiguration); + } + // Treasury burn canonical reasonable: 0-50% del treasury share. + if treasury_burn_bps > 5_000 { + return Err(Error::InvalidConfiguration); + } + + Ok(Self { + owner: Self::env().caller(), + treasury, + mint_fee_base, + creator_share_bps, + treasury_share_bps, + transfer_royalty_bps, + treasury_burn_bps, + max_supply, + lockup_blocks, + children_safety_strict, + total_supply: 0, + token_owner: Mapping::default(), + token_creator: Mapping::default(), + token_asset_hash: Mapping::default(), + token_mint_block: Mapping::default(), + owner_balance: Mapping::default(), + token_approved: Mapping::default(), + paused: false, + }) + } + + /// Default canonical constructor BM V2 ratified. + /// Treasury = caller (creator deployment + admin own Treasury initially). + /// Mint fee = 1 OGC (1_000_000_000_000_000_000 base units canonical = 10^18). + /// 91/9 split + 5% royalty + 20% treasury burn + unlimited supply. + #[ink(constructor)] + pub fn default_bm_v2() -> Self { + // Safe canonical — default values invariant-validated compile-detect. + Self::new( + Self::env().caller(), + 1_000_000_000_000_000_000, // 1 OGC base canonical + 9_100, // 91% creator + 900, // 9% treasury + 500, // 5% royalty + 2_000, // 20% treasury burn + None, // unlimited supply canonical + 0, // no lockup canonical default + false, // children safety strict opt-in + ) + .expect("BM V2 default constants canonical invariant-validated") + } + + // ───────────────────────────────────────────────────────────── + // Public messages canonical — Read methods (free per Inmutable #37) + // ───────────────────────────────────────────────────────────── + + /// Owner canonical del contract. + #[ink(message)] + pub fn owner(&self) -> AccountId { + self.owner + } + + /// Treasury address canonical. + #[ink(message)] + pub fn treasury(&self) -> AccountId { + self.treasury + } + + /// Total supply canonical hasta fecha. + #[ink(message)] + pub fn total_supply(&self) -> u32 { + self.total_supply + } + + /// Max supply canonical (None = unlimited). + #[ink(message)] + pub fn max_supply(&self) -> Option { + self.max_supply + } + + /// Mint fee base canonical (pre-HardwareTier modulation). + #[ink(message)] + pub fn mint_fee_base(&self) -> Balance { + self.mint_fee_base + } + + /// Owner canonical del token específico. + #[ink(message)] + pub fn token_owner(&self, token_id: TokenId) -> Option { + self.token_owner.get(token_id) + } + + /// Original creator canonical del token (immutable post-mint). + #[ink(message)] + pub fn token_creator(&self, token_id: TokenId) -> Option { + self.token_creator.get(token_id) + } + + /// Asset hash BLAKE3 canonical del token. + #[ink(message)] + pub fn token_asset_hash(&self, token_id: TokenId) -> Option { + self.token_asset_hash.get(token_id) + } + + /// Balance (token count) canonical del account. + #[ink(message)] + pub fn balance_of(&self, account: AccountId) -> u32 { + self.owner_balance.get(account).unwrap_or(0) + } + + /// Approved spender canonical del token específico. + #[ink(message)] + pub fn approved_spender(&self, token_id: TokenId) -> Option { + self.token_approved.get(token_id) + } + + /// Paused state canonical. + #[ink(message)] + pub fn is_paused(&self) -> bool { + self.paused + } + + // ───────────────────────────────────────────────────────────── + // Public messages canonical — Mint + // ───────────────────────────────────────────────────────────── + + /// Mint canonical new NFT. Payment value debe ≥ mint_fee_base. + /// + /// Flow canonical: + /// 1. Verify paused == false (Error::ContractPaused) + /// 2. Verify total_supply < max_supply si canonical capped + /// 3. Verify asset_hash != [0; 32] (Inmutable #26 canonical) + /// 4. Verify payment ≥ mint_fee_base (Error::InsufficientPayment) + /// 5. Split payment 91/9 canonical (BM V2 ratified) + /// 6. Burn 20% del treasury share canonical (BM V2 burn matrix) + /// 7. Record token canonical state (owner + creator + hash + mint_block) + /// 8. Emit Minted event canonical + #[ink(message, payable)] + pub fn mint(&mut self, asset_hash: AssetHash) -> Result { + if self.paused { + return Err(Error::ContractPaused); + } + if let Some(max) = self.max_supply { + if self.total_supply >= max { + return Err(Error::MaxSupplyReached); + } + } + if asset_hash == [0u8; 32] { + return Err(Error::InvalidAssetHash); + } + let payment = self.env().transferred_value(); + if payment < self.mint_fee_base { + return Err(Error::InsufficientPayment); + } + + // BM V2 canonical 91/9 split — basis points arithmetic. + let creator_share = payment + .saturating_mul(self.creator_share_bps as Balance) + .saturating_div(10_000); + let treasury_share = payment + .saturating_mul(self.treasury_share_bps as Balance) + .saturating_div(10_000); + + // Treasury burn canonical (per BM V2 burn matrix Pillar 3). + let treasury_burn = treasury_share + .saturating_mul(self.treasury_burn_bps as Balance) + .saturating_div(10_000); + let treasury_net = treasury_share.saturating_sub(treasury_burn); + + // Transfer canonical funds: creator + treasury_net + burn (via send to zero). + let creator = self.env().caller(); + self.env() + .transfer(creator, creator_share) + .map_err(|_| Error::InsufficientPayment)?; + self.env() + .transfer(self.treasury, treasury_net) + .map_err(|_| Error::InsufficientPayment)?; + // Burn portion canonical: transfer a zero address (ink! v6 burn pattern). + // Real Origin Chain canonical burn vía pallet-balances::burn extrinsic + // post Phase 3.x integration. Slice 3.2 SCAFFOLD interim: zero address. + let zero = AccountId::from([0u8; 32]); + self.env() + .transfer(zero, treasury_burn) + .map_err(|_| Error::InsufficientPayment)?; + + // Mint canonical state update. + let token_id = self.total_supply; + self.token_owner.insert(token_id, &creator); + self.token_creator.insert(token_id, &creator); + self.token_asset_hash.insert(token_id, &asset_hash); + self.token_mint_block + .insert(token_id, &self.env().block_number()); + let new_balance = self.balance_of(creator).saturating_add(1); + self.owner_balance.insert(creator, &new_balance); + self.total_supply = self.total_supply.saturating_add(1); + + self.env().emit_event(Minted { + token_id, + creator, + asset_hash, + mint_fee_paid: payment, + creator_share, + treasury_share: treasury_net, + treasury_burned: treasury_burn, + }); + + Ok(token_id) + } + + // ───────────────────────────────────────────────────────────── + // Public messages canonical — Transfer + Approve + // ───────────────────────────────────────────────────────────── + + /// Transfer canonical de token con sale_price (secondary sale). + /// Royalty 5% canonical paid al original creator from sale_price. + /// + /// Flow canonical: + /// 1. Verify paused == false + /// 2. Verify token exists canonical + /// 3. Verify caller == owner OR caller == approved + /// 4. Verify lockup canonical expired (block_now >= mint_block + lockup_blocks) + /// 5. Verify from != to (Error::SelfTransferProhibited) + /// 6. Calculate royalty canonical (transfer_royalty_bps × sale_price / 10000) + /// 7. Transfer royalty canonical al original_creator + /// 8. Update ownership canonical state + /// 9. Clear approval canonical post-transfer + /// 10. Emit Transferred event canonical + #[ink(message, payable)] + pub fn transfer(&mut self, token_id: TokenId, to: AccountId) -> Result<()> { + if self.paused { + return Err(Error::ContractPaused); + } + let from = self + .token_owner + .get(token_id) + .ok_or(Error::TokenNotFound)?; + let caller = self.env().caller(); + if caller != from && self.token_approved.get(token_id) != Some(caller) { + return Err(Error::NotApprovedSpender); + } + if from == to { + return Err(Error::SelfTransferProhibited); + } + let mint_block = self + .token_mint_block + .get(token_id) + .ok_or(Error::TokenNotFound)?; + let now = self.env().block_number(); + if now < mint_block.saturating_add(self.lockup_blocks) { + return Err(Error::TokenLocked); + } + + // Royalty canonical: pay 5% al original creator (secondary sale). + let sale_price = self.env().transferred_value(); + let royalty = sale_price + .saturating_mul(self.transfer_royalty_bps as Balance) + .saturating_div(10_000); + if royalty > 0 { + let original_creator = self + .token_creator + .get(token_id) + .ok_or(Error::TokenNotFound)?; + self.env() + .transfer(original_creator, royalty) + .map_err(|_| Error::InsufficientPayment)?; + } + // Remaining canonical sale_price → seller (from). + let to_seller = sale_price.saturating_sub(royalty); + if to_seller > 0 { + self.env() + .transfer(from, to_seller) + .map_err(|_| Error::InsufficientPayment)?; + } + + // Ownership canonical update. + self.token_owner.insert(token_id, &to); + let from_balance = self.balance_of(from).saturating_sub(1); + self.owner_balance.insert(from, &from_balance); + let to_balance = self.balance_of(to).saturating_add(1); + self.owner_balance.insert(to, &to_balance); + self.token_approved.remove(token_id); // clear approval post-transfer + + self.env().emit_event(Transferred { + token_id, + from, + to, + sale_price, + royalty_paid: royalty, + }); + + Ok(()) + } + + /// Approve canonical spender para token específico. + /// Standard PSP34-like approval pattern. + #[ink(message)] + pub fn approve(&mut self, token_id: TokenId, spender: AccountId) -> Result<()> { + if self.paused { + return Err(Error::ContractPaused); + } + let owner = self + .token_owner + .get(token_id) + .ok_or(Error::TokenNotFound)?; + if owner != self.env().caller() { + return Err(Error::NotTokenOwner); + } + self.token_approved.insert(token_id, &spender); + self.env().emit_event(Approved { + token_id, + owner, + spender, + }); + Ok(()) + } + + // ───────────────────────────────────────────────────────────── + // Admin messages canonical — Owner-only governance actions + // ───────────────────────────────────────────────────────────── + + /// Emergency pause canonical (owner-only). Blocks mint + transfer. + /// Unpause canonical via governance Phase 5 DAO vote post-shipping + /// canonical Capacity #4 OCC Treasury. + #[ink(message)] + pub fn emergency_pause(&mut self) -> Result<()> { + if self.env().caller() != self.owner { + return Err(Error::NotContractOwner); + } + self.paused = true; + self.env().emit_event(PauseToggled { + actor: self.owner, + paused: true, + }); + Ok(()) + } + + /// Unpause canonical (owner-only Slice 3.2 SCAFFOLD interim). + /// Phase 3.x maduración: require governance vote via Phase 5 pallet. + #[ink(message)] + pub fn unpause(&mut self) -> Result<()> { + if self.env().caller() != self.owner { + return Err(Error::NotContractOwner); + } + self.paused = false; + self.env().emit_event(PauseToggled { + actor: self.owner, + paused: false, + }); + Ok(()) + } + } + + // ───────────────────────────────────────────────────────────────── + // Tests canonical (compile-only — real deploy testing Phase 3.x post + // cargo-contract v6 install canonical authorized). + // ───────────────────────────────────────────────────────────────── + + #[cfg(test)] + mod tests { + use super::*; + + /// Constructor default BM V2 canonical invariant test. + /// Verifies 91/9 split + 5% royalty + 20% burn canonical defaults. + #[ink::test] + fn default_bm_v2_constructor_canonical_invariants() { + let contract = OriginNftMint::default_bm_v2(); + assert_eq!(contract.total_supply(), 0); + assert_eq!(contract.mint_fee_base(), 1_000_000_000_000_000_000); // 1 OGC + assert_eq!(contract.creator_share_bps, 9_100); + assert_eq!(contract.treasury_share_bps, 900); + assert_eq!(contract.transfer_royalty_bps, 500); + assert_eq!(contract.treasury_burn_bps, 2_000); + assert_eq!(contract.max_supply(), None); + assert!(!contract.is_paused()); + } + + /// Invariant canonical: creator + treasury shares MUST sum to 10_000 bps (100%). + #[ink::test] + fn invalid_share_split_rejected_canonical() { + let result = OriginNftMint::new( + AccountId::from([0u8; 32]), + 1_000_000_000_000_000_000, + 9_000, // 90% — invalid sum + 500, // 5% + 500, + 2_000, + None, + 0, + false, + ); + assert_eq!(result.err(), Some(Error::InvalidConfiguration)); + } + + /// Invariant canonical: transfer_royalty_bps must be ≤ 1_000 (10% max industry standard). + #[ink::test] + fn excessive_royalty_rejected_canonical() { + let result = OriginNftMint::new( + AccountId::from([0u8; 32]), + 1_000_000_000_000_000_000, + 9_100, + 900, + 1_500, // 15% — exceeds max + 2_000, + None, + 0, + false, + ); + assert_eq!(result.err(), Some(Error::InvalidConfiguration)); + } + } +}