use crate::errors::VmError; use crate::rewards::{community_split, genesis_split, SHELL_PER_NUT, NODE_REWARD_NUT}; use nu_state::{ nft::NftState, story_node::{NodeStatus, StoryNodeState, WeightedVote}, validator::ValidatorState, StateAccessor, }; pub struct ExecutionContext<'a> { pub state: &'a dyn StateAccessor, pub block_height: u64, pub now_ms: i64, } // entry_fee = 25% of reward = 25 NUT = 25_000 Shell const ENTRY_FEE_SHELL: u64 = (NODE_REWARD_NUT / 4) * SHELL_PER_NUT; // vote_stake_lock = 10% of reward = 10 NUT = 10_000 Shell const VOTE_LOCK_SHELL: u64 = (NODE_REWARD_NUT / 10) * SHELL_PER_NUT; pub fn execute_token_transfer( ctx: &ExecutionContext, sender: &str, to: &str, amount: u64, fee: u64, nonce: u64, ) -> Result<(), VmError> { let expected_nonce = ctx.state.get_nonce(sender); if nonce != expected_nonce { return Err(VmError::InvalidNonce { expected: expected_nonce, got: nonce }); } let balance = ctx.state.get_balance(sender); let total = amount.checked_add(fee).ok_or(VmError::Unknown("overflow".into()))?; if balance < total { return Err(VmError::InsufficientBalance { need: total, have: balance }); } let recipient_balance = ctx.state.get_balance(to); ctx.state.set_balance(sender, balance - total); ctx.state.set_balance(to, recipient_balance + amount); ctx.state.inc_nonce(sender); Ok(()) } /// Validates entry fee, creates a Pending StoryNodeState. /// Canonical node_id is assigned at approval — temp_id used until then. pub fn execute_node_submit( ctx: &ExecutionContext, sender: &str, nonce: u64, fee: u64, story_id: &str, parent_node_id: &str, content_hash: &str, temp_id: &str, ) -> Result<(), VmError> { let expected_nonce = ctx.state.get_nonce(sender); if nonce != expected_nonce { return Err(VmError::InvalidNonce { expected: expected_nonce, got: nonce }); } let total = ENTRY_FEE_SHELL.checked_add(fee).ok_or(VmError::Unknown("overflow".into()))?; let balance = ctx.state.get_balance(sender); if balance < total { return Err(VmError::InsufficientBalance { need: total, have: balance }); } // Deduct entry fee + tx fee ctx.state.set_balance(sender, balance - total); ctx.state.inc_nonce(sender); let node = StoryNodeState { node_id: String::new(), // assigned at approval temp_id: temp_id.to_string(), story_id: story_id.to_string(), parent_id: parent_node_id.to_string(), author: sender.to_string(), content_hash: content_hash.to_string(), status: NodeStatus::Pending, submitted_at: ctx.now_ms, vote_open_at: 0, vote_end_at: 0, votes: Vec::new(), nft_id: String::new(), entry_fee: ENTRY_FEE_SHELL, }; ctx.state.set_node(&node); Ok(()) } /// Locks stake so the voter can cast a vote on the given node. /// Stake is released when the voting period ends (approval or rejection). pub fn execute_vote_register( ctx: &ExecutionContext, sender: &str, nonce: u64, fee: u64, node_temp_id: &str, ) -> Result<(), VmError> { let expected_nonce = ctx.state.get_nonce(sender); if nonce != expected_nonce { return Err(VmError::InvalidNonce { expected: expected_nonce, got: nonce }); } let node = ctx.state.get_node_by_temp(node_temp_id) .ok_or_else(|| VmError::NodeNotFound(node_temp_id.to_string()))?; if node.status != NodeStatus::VotingOpen { return Err(VmError::NodeNotVoting); } // Check not already registered if node.votes.iter().any(|v| v.voter == sender) { return Err(VmError::AlreadyVoted); } let total = VOTE_LOCK_SHELL.checked_add(fee).ok_or(VmError::Unknown("overflow".into()))?; let balance = ctx.state.get_balance(sender); if balance < total { return Err(VmError::InsufficientBalance { need: total, have: balance }); } ctx.state.set_balance(sender, balance - fee); ctx.state.lock_stake(sender, VOTE_LOCK_SHELL, node.vote_end_at); ctx.state.inc_nonce(sender); Ok(()) } /// Records the voter's choice. Weight computed from staked + PoN score. /// Validates voter was registered (has a pending vote entry). pub fn execute_vote_cast( ctx: &ExecutionContext, sender: &str, nonce: u64, fee: u64, node_temp_id: &str, approve: bool, ) -> Result<(), VmError> { let expected_nonce = ctx.state.get_nonce(sender); if nonce != expected_nonce { return Err(VmError::InvalidNonce { expected: expected_nonce, got: nonce }); } let node = ctx.state.get_node_by_temp(node_temp_id) .ok_or_else(|| VmError::NodeNotFound(node_temp_id.to_string()))?; if node.status != NodeStatus::VotingOpen { return Err(VmError::NodeNotVoting); } // Voter must have registered (stake locked) — check no existing approve/reject cast let already_cast = node.votes.iter().any(|v| v.voter == sender && v.weight > 0.0); if already_cast { return Err(VmError::AlreadyVoted); } let balance = ctx.state.get_balance(sender); if balance < fee { return Err(VmError::InsufficientBalance { need: fee, have: balance }); } let account = ctx.state.get_account(sender); let weight = (account.staked as f64).sqrt() * account.pon_score; ctx.state.set_balance(sender, balance - fee); ctx.state.inc_nonce(sender); ctx.state.record_vote(node_temp_id, WeightedVote { voter: sender.to_string(), approve, weight }); Ok(()) } /// Stake or unstake NUT tokens. pub fn execute_stake_op( ctx: &ExecutionContext, sender: &str, nonce: u64, fee: u64, stake: bool, amount: u64, ) -> Result<(), VmError> { let expected_nonce = ctx.state.get_nonce(sender); if nonce != expected_nonce { return Err(VmError::InvalidNonce { expected: expected_nonce, got: nonce }); } let mut account = ctx.state.get_account(sender); let total_fee_deduct = fee; if stake { let total = amount.checked_add(total_fee_deduct).ok_or(VmError::Unknown("overflow".into()))?; if account.balance < total { return Err(VmError::InsufficientBalance { need: total, have: account.balance }); } account.balance -= total; account.staked += amount; } else { if account.balance < total_fee_deduct { return Err(VmError::InsufficientBalance { need: total_fee_deduct, have: account.balance }); } if account.staked < amount { return Err(VmError::InsufficientBalance { need: amount, have: account.staked }); } account.balance -= total_fee_deduct; account.staked -= amount; account.balance += amount; } account.nonce += 1; ctx.state.set_account(&account); Ok(()) } /// Register as a validator — locks stake, creates ValidatorState. pub fn execute_validator_register( ctx: &ExecutionContext, sender: &str, nonce: u64, fee: u64, stake: u64, ) -> Result<(), VmError> { use nu_consensus::types::MIN_VALIDATOR_STAKE; let expected_nonce = ctx.state.get_nonce(sender); if nonce != expected_nonce { return Err(VmError::InvalidNonce { expected: expected_nonce, got: nonce }); } if stake < MIN_VALIDATOR_STAKE { return Err(VmError::StakeTooLow); } let mut account = ctx.state.get_account(sender); let total = stake.checked_add(fee).ok_or(VmError::Unknown("overflow".into()))?; if account.balance < total { return Err(VmError::InsufficientBalance { need: total, have: account.balance }); } account.balance -= total; account.staked += stake; account.nonce += 1; ctx.state.set_account(&account); let record = ValidatorState::new(sender.to_string(), stake); ctx.state.set_validator(&record); Ok(()) } const BURN_WALLET: &str = "0x000000000000000000000000000000000000DEAD"; const GENESIS_STORY: &str = "genesis"; /// Auto-tx: transition node from Pending → VotingOpen. pub fn execute_voting_open( ctx: &ExecutionContext, node_id: &str, ) -> Result<(), VmError> { let mut node = ctx.state.get_node_by_temp(node_id) .ok_or_else(|| VmError::NodeNotFound(node_id.to_string()))?; if node.status != NodeStatus::Pending { return Err(VmError::Unknown("node not in Pending state".into())); } let voting_period_ms: i64 = 3 * 24 * 60 * 60 * 1_000; node.status = NodeStatus::VotingOpen; node.vote_open_at = ctx.now_ms; node.vote_end_at = ctx.now_ms + voting_period_ms; ctx.state.set_node(&node); Ok(()) } /// Auto-tx: approve node, mint NFT, distribute rewards. pub fn execute_node_approve( ctx: &ExecutionContext, temp_id: &str, canonical_id: &str, dev_wallet: &str, ) -> Result<(), VmError> { let mut node = ctx.state.get_node_by_temp(temp_id) .ok_or_else(|| VmError::NodeNotFound(temp_id.to_string()))?; if node.status != NodeStatus::VotingOpen { return Err(VmError::NodeNotVoting); } node.status = NodeStatus::Approved; node.node_id = canonical_id.to_string(); node.nft_id = canonical_id.to_string(); ctx.state.set_node(&node); // Mint NFT let depth = canonical_id.len() as u32; let nft = NftState { nft_id: canonical_id.to_string(), node_id: canonical_id.to_string(), owner: node.author.clone(), collection_id: String::new(), depth, lineage: vec![], minted_at: ctx.now_ms, }; ctx.state.set_nft(&nft); ctx.state.add_nft(&node.author, canonical_id); // Unlock voter stakes + update PoN scores let winning_approve = node.is_approved(); let total_weight: f64 = node.votes.iter().map(|v| v.weight).sum(); let mut voter_reward_pool = 0u64; if node.story_id == GENESIS_STORY { let reward = genesis_split(); let dw_bal = ctx.state.get_balance(dev_wallet); ctx.state.set_balance(dev_wallet, dw_bal + reward); } else { let split = community_split(); voter_reward_pool = split.voters; // Author reward let author_bal = ctx.state.get_balance(&node.author); ctx.state.set_balance(&node.author, author_bal + split.author); // Burn let burn_bal = ctx.state.get_balance(BURN_WALLET); ctx.state.set_balance(BURN_WALLET, burn_bal + split.burn); } // Distribute voter rewards + unlock stakes for vote in &node.votes { ctx.state.unlock_stake(&vote.voter, VOTE_LOCK_SHELL); if node.story_id != GENESIS_STORY && vote.approve == winning_approve && total_weight > 0.0 { let share = (vote.weight / total_weight * voter_reward_pool as f64) as u64; let bal = ctx.state.get_balance(&vote.voter); ctx.state.set_balance(&vote.voter, bal + share); } } Ok(()) } /// Auto-tx: reject node, burn entry fee, unlock stakes. pub fn execute_node_reject( ctx: &ExecutionContext, node_id: &str, ) -> Result<(), VmError> { let mut node = ctx.state.get_node_by_temp(node_id) .ok_or_else(|| VmError::NodeNotFound(node_id.to_string()))?; if node.status != NodeStatus::VotingOpen { return Err(VmError::NodeNotVoting); } node.status = NodeStatus::Rejected; ctx.state.set_node(&node); // Burn entry fee let burn_bal = ctx.state.get_balance(BURN_WALLET); ctx.state.set_balance(BURN_WALLET, burn_bal + node.entry_fee); // Unlock all voter stakes for vote in &node.votes { ctx.state.unlock_stake(&vote.voter, VOTE_LOCK_SHELL); } Ok(()) }