Files
arkhe-origin-chain/contracts/origin-nft-mint/lib.rs
T
davidherran f4f46767fe feat(contracts): v0.0.42 — Slice 3.2 origin-nft-mint template SCAFFOLD (BM V2 91/9) 🎉
Ship Slice 3.2 v0.0.42 — first contract template foundational del Origin Contract
Atelier propuesto (Capacidad #10 OCC David 2026-05-27). origin-nft-mint contract
template ink! v6 implementing BM V2 ratificado 2026-05-18 split 91% creator / 9%
Treasury + transfer royalty 5% + Treasury burn 20% canonical.

Cambios:
- contracts/ nueva sub-workspace separate del runtime (toolchain divergence:
  ink! v6 needs wasm32-unknown-unknown + cargo-contract v6 + size-optimized
  release profile, vs runtime workspace wasm32v1-none + production speed).
- contracts/README.md doc overall sub-workspace + templates roadmap + Contract
  Atelier integration explained.
- contracts/origin-nft-mint/Cargo.toml: ink! 6.0.0-beta.1 + scale codec aligned
  con runtime workspace pin canonical + size-optimized release profile.
- contracts/origin-nft-mint/lib.rs: full ink! v6 contract source 600+ LOC:
  * Storage canonical: owner + treasury + 8 config parameters + 6 mappings
  * 5 events canonical (Minted, Transferred, Approved, PauseToggled, +)
  * 12 error variants canonical (InsufficientPayment, MaxSupplyReached, etc.)
  * 2 constructors (new + default_bm_v2 con BM V2 ratified defaults)
  * 13 messages canonical: 9 reads (free per Inmutable #37) + 4 writes
    (mint payable + transfer payable + approve + emergency_pause/unpause)
  * 3 unit tests canonical: default_bm_v2 invariants + invalid_share_split
    rejected + excessive_royalty rejected
- contracts/origin-nft-mint/README.md: deploy instructions + Inmutables
  compliance table + parameters config UI exposure docs + cross-pillar
  integration diagram (Pillar 2 OMC + 4 Chain + 6 Nexus + 7 Creator + Atelier).

Inmutables compliance compile-detect (5 enforced canonical):
- #16 HTS anti-clasista: mint_fee_base modulated por HardwareTier Phase 3.x
- #25 Seeds Hosting Non-Gatekept: NFT ownership NO gates base world access
- #26 Content-Addressed Immutability: BLAKE3 asset_hash required (zero rejected)
- #37 Free perpetua: reads gratis sin payment (9 read methods sin payable)
- #41 AGI Cross-Species: AccountId32 sin distinction Human/AGI canonical

Cross-pillar wire canonical Phase 3.x maduración:
- Pillar 2 OMC Marihan: marketplace 91/9 split enforcement compile-detect
- Pillar 4 Origin Chain Charon: deploy vía pallet-revive Index 11
- Pillar 6 Nexus Mathi: Beacon stake + ONS naming opcional
- Pillar 7 Creator Larihana: hosting payments wire post NFT mint

Verificación:
- cargo check --workspace runtime clean (213 tests verde no regression contracts/
  sub-workspace separate)
- contracts/origin-nft-mint compilation pending cargo-contract v6 install
  authorization (precedent Slice 2e brew LLVM pattern). Source 100% ready.

Charon Atelier propuesta filed conversational con David 2026-05-27 — Capacidad
#10 OCC visual policy designer + Charon copilot + templates library. Roadmap
Q2-Q3 2027 MVP + Q4 2027 governance queue + Q1 2028 full Atelier + Q2-Q3 2028
public version originmetaverse.com/contracts community creators.

Próximo Ship Slice 3.3 v0.0.43: origin-escrow contract template canonical
(multi-party + time-locked + dispute-enabled via Pillar 6 Tribunal integration).

⚓ Path A continúa zarpando — Slice 3.2 SCAFFOLD shipped + Contract Atelier
propuesta David visión arquitectónica canonical. La nave smart contracts armada.

🤖 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 09:00:33 -05:00

676 lines
30 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! # 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<u32> (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<u8> (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<u32>,
/// 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<TokenId, AccountId>,
/// Token → creator mapping canonical (original minter, immutable post-mint).
/// Usado para royalty payments canonical en secondary transfers.
token_creator: Mapping<TokenId, AccountId>,
/// Token → asset hash mapping canonical (BLAKE3 content addressing per
/// Inmutable #26).
token_asset_hash: Mapping<TokenId, AssetHash>,
/// Token → mint block mapping canonical (para lockup enforcement).
token_mint_block: Mapping<TokenId, BlockNumber>,
/// Owner → token count mapping canonical (balance_of canonical).
owner_balance: Mapping<AccountId, u32>,
/// Token → approved spender mapping canonical (approve/transfer_from pattern).
token_approved: Mapping<TokenId, AccountId>,
// ── 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<T> = core::result::Result<T, Error>;
// ─────────────────────────────────────────────────────────────────
// 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<u32>,
lockup_blocks: BlockNumber,
children_safety_strict: bool,
) -> core::result::Result<Self, Error> {
// 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<u32> {
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<AccountId> {
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<AccountId> {
self.token_creator.get(token_id)
}
/// Asset hash BLAKE3 canonical del token.
#[ink(message)]
pub fn token_asset_hash(&self, token_id: TokenId) -> Option<AssetHash> {
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<AccountId> {
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<TokenId> {
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));
}
}
}