forked from Keelrleek/SoulStream
133 lines
5.0 KiB
Rust
133 lines
5.0 KiB
Rust
//! The SoulStream holdings contract. Single writer: one member's latest
|
|
//! signed album list in one Space, at the address (space id, member key).
|
|
//! Merge is the pointer contract's total order, (sequence, blake3) —
|
|
//! `persona_state::holdings::merge` decides, so every delivery order
|
|
//! converges, and a stale replay cannot bring a withdrawn list back.
|
|
//!
|
|
//! The body is opaque here: sealed to the room's epoch key in a private
|
|
//! Space, and the contract only ever checks the envelope and its signature.
|
|
//!
|
|
//! Build: `cargo build --release --target wasm32-unknown-unknown` in this
|
|
//! directory; copy `target/wasm32-unknown-unknown/release/
|
|
//! soulstream_holdings_contract.wasm` to `src-tauri/assets/holdings_contract.wasm`
|
|
//! and re-pin the code hash in `src-tauri/src/holdings.rs`.
|
|
|
|
use freenet_stdlib::prelude::*;
|
|
use persona_state::holdings as t;
|
|
|
|
pub struct Contract;
|
|
|
|
fn parts(parameters: &Parameters<'static>) -> Result<([u8; 16], [u8; 32]), ContractError> {
|
|
t::params_parts(parameters.as_ref())
|
|
.ok_or_else(|| ContractError::Other("params are not family+space+member".into()))
|
|
}
|
|
|
|
fn summary_of(bytes: &[u8], s: &t::HoldingsState) -> Vec<u8> {
|
|
let (seq, hash) = t::rank(bytes, s);
|
|
let mut out = Vec::with_capacity(40);
|
|
out.extend_from_slice(&seq.to_le_bytes());
|
|
out.extend_from_slice(&hash);
|
|
out
|
|
}
|
|
|
|
#[contract]
|
|
impl ContractInterface for Contract {
|
|
fn validate_state(
|
|
parameters: Parameters<'static>,
|
|
state: State<'static>,
|
|
_related: RelatedContracts<'static>,
|
|
) -> Result<ValidateResult, ContractError> {
|
|
let (space_id, member) = parts(¶meters)?;
|
|
if state.as_ref().is_empty() {
|
|
return Ok(ValidateResult::Valid);
|
|
}
|
|
match t::validate(&space_id, &member, state.as_ref()) {
|
|
Ok(_) => Ok(ValidateResult::Valid),
|
|
Err(e) => Err(ContractError::Other(format!("invalid holdings: {e}"))),
|
|
}
|
|
}
|
|
|
|
fn update_state(
|
|
parameters: Parameters<'static>,
|
|
state: State<'static>,
|
|
data: Vec<UpdateData<'static>>,
|
|
) -> Result<UpdateModification<'static>, ContractError> {
|
|
let (space_id, member) = parts(¶meters)?;
|
|
let mut candidates: Vec<Vec<u8>> = Vec::new();
|
|
if !state.as_ref().is_empty() {
|
|
candidates.push(state.as_ref().to_vec());
|
|
}
|
|
let mut incoming_valid = 0usize;
|
|
for ud in &data {
|
|
let mut push = |bytes: &[u8]| {
|
|
if bytes.is_empty() {
|
|
return;
|
|
}
|
|
if t::validate(&space_id, &member, bytes).is_ok() {
|
|
incoming_valid += 1;
|
|
}
|
|
candidates.push(bytes.to_vec());
|
|
};
|
|
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());
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
if incoming_valid == 0 && !data.is_empty() {
|
|
return Err(ContractError::InvalidUpdate);
|
|
}
|
|
match t::merge(&space_id, &member, candidates.iter().map(|v| v.as_slice())) {
|
|
Some(bytes) => Ok(UpdateModification::valid(State::from(bytes))),
|
|
None => Ok(UpdateModification::valid(state)),
|
|
}
|
|
}
|
|
|
|
fn summarize_state(
|
|
parameters: Parameters<'static>,
|
|
state: State<'static>,
|
|
) -> Result<StateSummary<'static>, ContractError> {
|
|
let (space_id, member) = parts(¶meters)?;
|
|
if state.as_ref().is_empty() {
|
|
return Ok(StateSummary::from(Vec::new()));
|
|
}
|
|
let s = t::validate(&space_id, &member, state.as_ref())
|
|
.map_err(|e| ContractError::Other(format!("invalid holdings: {e}")))?;
|
|
Ok(StateSummary::from(summary_of(state.as_ref(), &s)))
|
|
}
|
|
|
|
fn get_state_delta(
|
|
parameters: Parameters<'static>,
|
|
state: State<'static>,
|
|
summary: StateSummary<'static>,
|
|
) -> Result<StateDelta<'static>, ContractError> {
|
|
let (space_id, member) = parts(¶meters)?;
|
|
if state.as_ref().is_empty() {
|
|
return Ok(StateDelta::from(Vec::new()));
|
|
}
|
|
let s = t::validate(&space_id, &member, state.as_ref())
|
|
.map_err(|e| ContractError::Other(format!("invalid holdings: {e}")))?;
|
|
let ours = t::rank(state.as_ref(), &s);
|
|
let theirs = {
|
|
let b = summary.as_ref();
|
|
if b.len() == 40 {
|
|
let seq = u64::from_le_bytes(b[..8].try_into().unwrap());
|
|
let mut h = [0u8; 32];
|
|
h.copy_from_slice(&b[8..40]);
|
|
Some((seq, h))
|
|
} else {
|
|
None
|
|
}
|
|
};
|
|
let newer = match theirs {
|
|
Some(t) => ours > t,
|
|
None => true,
|
|
};
|
|
Ok(StateDelta::from(if newer { state.as_ref().to_vec() } else { Vec::new() }))
|
|
}
|
|
}
|