//! The SoulStream proposal-board contract. Multi-writer: the owner offers //! checkpoints and decisions, contributors offer proposals; `persona_state::proposals::merge_board` //! is the bouncer (owner signature, the current roster, decided-tombstones, //! the caps). Deterministic by construction, so every delivery order //! converges; a state that does not merge to itself is refused. //! //! Build: `cargo build --release --target wasm32-unknown-unknown` in this //! directory; copy `target/wasm32-unknown-unknown/release/ //! soulstream_proposal_contract.wasm` to `src-tauri/assets/proposal_contract.wasm` //! and re-pin the code hash in `src-tauri/src/proposals.rs`. use freenet_stdlib::prelude::*; use persona_state::proposals as t; pub struct Contract; fn parts(parameters: &Parameters<'static>) -> Result<([u8; 32], [u8; 16]), ContractError> { t::params_parts(parameters.as_ref()) .ok_or_else(|| ContractError::Other("params are not family+owner+uuid".into())) } fn collect<'a>(state: &'a State<'static>, data: &'a [UpdateData<'static>]) -> (Vec<&'a [u8]>, bool) { let mut candidates: Vec<&[u8]> = Vec::new(); if !state.as_ref().is_empty() { candidates.push(state.as_ref()); } let mut any_parse = false; let mut push = |b: &'a [u8]| { if !b.is_empty() { any_parse |= bincode::deserialize::(b).is_ok(); candidates.push(b); } }; for ud in data { match ud { UpdateData::State(s) => push(s.as_ref()), UpdateData::Delta(d) => push(d.as_ref()), UpdateData::StateAndDelta { state, delta } => { push(state.as_ref()); push(delta.as_ref()); } _ => {} } } (candidates, any_parse) } #[contract] impl ContractInterface for Contract { fn validate_state( parameters: Parameters<'static>, state: State<'static>, _related: RelatedContracts<'static>, ) -> Result { let (owner, uuid) = parts(¶meters)?; let bytes = state.as_ref(); if bytes.is_empty() { return Ok(ValidateResult::Valid); } if bytes.len() > t::MAX_STATE_BYTES { return Err(ContractError::Other(format!( "board state {} bytes over cap {}", bytes.len(), t::MAX_STATE_BYTES ))); } let merged = t::merge_board(&owner, &uuid, &[bytes]); let canon = bincode::serialize(&merged).map_err(|e| ContractError::Deser(e.to_string()))?; if canon != bytes { return Err(ContractError::Other( "state is not canonical (unsigned, off-roster, decided, or over a cap)".into(), )); } Ok(ValidateResult::Valid) } fn update_state( parameters: Parameters<'static>, state: State<'static>, data: Vec>, ) -> Result, ContractError> { let (owner, uuid) = parts(¶meters)?; let (candidates, any_parse) = collect(&state, &data); if !any_parse && !data.is_empty() { return Err(ContractError::InvalidUpdate); } let merged = t::merge_board(&owner, &uuid, &candidates); let bytes = bincode::serialize(&merged).map_err(|e| ContractError::Deser(e.to_string()))?; Ok(UpdateModification::valid(State::from(bytes))) } fn summarize_state( parameters: Parameters<'static>, state: State<'static>, ) -> Result, ContractError> { let _ = parts(¶meters)?; if state.as_ref().is_empty() { return Ok(StateSummary::from(Vec::new())); } Ok(StateSummary::from(blake3::hash(state.as_ref()).as_bytes().to_vec())) } fn get_state_delta( parameters: Parameters<'static>, state: State<'static>, summary: StateSummary<'static>, ) -> Result, ContractError> { let _ = parts(¶meters)?; if state.as_ref().is_empty() { return Ok(StateDelta::from(Vec::new())); } if summary.as_ref() == blake3::hash(state.as_ref()).as_bytes() { Ok(StateDelta::from(Vec::new())) } else { Ok(StateDelta::from(state.as_ref().to_vec())) } } }