lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

commit eec737f2f8c7ebe2271a8c567be29cc77ec9c9ae
parent 4d9dfb1e71d5cb14eb62fa31b0109d346138f7e2
Author: triesap <tyson@radroots.org>
Date:   Thu, 30 Jul 2026 07:51:43 +0000

trade: organize trade model and validation

- expose model evidence and reducer types from their final modules
- seal serialized trade structures behind constructors and accessors
- add intentional root aliases for the durable reducer surface
- lock module ownership privacy and serde behavior with API coverage

Diffstat:
Mcrates/trade/src/evidence.rs | 6++++++
Mcrates/trade/src/lib.rs | 27++++++++++++++++++++++++++-
Mcrates/trade/src/model.rs | 6++++++
Mcrates/trade/src/reducer.rs | 5+++++
Mcrates/trade/src/workflow.rs | 349++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Mcrates/trade/tests/package_boundary.rs | 52+++++++++++++++++++++++++++++++++++++++++++++++++++-
6 files changed, 397 insertions(+), 48 deletions(-)

diff --git a/crates/trade/src/evidence.rs b/crates/trade/src/evidence.rs @@ -1 +1,7 @@ //! Evidence types used by deterministic trade reduction. + +pub use crate::trade_contract_v1::{ + RadrootsTradeAttestationRecordV1, RadrootsTradeAttestationResultV1, + RadrootsTradeEvidenceStateV1, RadrootsTradeMutationRecordV1, + RadrootsTradePrivateTermsEvidenceV1, +}; diff --git a/crates/trade/src/lib.rs b/crates/trade/src/lib.rs @@ -11,7 +11,32 @@ pub mod model; pub mod operational_listing; pub mod prelude; pub mod reducer; +#[path = "workflow.rs"] +mod trade_contract_v1; pub mod validation; #[cfg(feature = "serde_json")] pub mod validation_receipt; -pub mod workflow; + +/// Side-effect-free trade workflow contracts. +/// +/// The versioned reexports are a temporary migration surface for the existing +/// serialized contract. New reducer consumers should use [`crate::model`], +/// [`crate::evidence`], and [`crate::reducer`]. +pub mod workflow { + pub use crate::trade_contract_v1::{ + RADROOTS_TRADE_REDUCER_CONTRACT_ID, RADROOTS_TRADE_REDUCER_VERSION, + RadrootsTradeAgreementClaimV1, RadrootsTradeAgreementStateV1, + RadrootsTradeAttestationRecordV1, RadrootsTradeAttestationResultV1, + RadrootsTradeAttestationStateV1, RadrootsTradeConflictStateV1, + RadrootsTradeEvidenceStateV1, RadrootsTradeFulfillmentStateV1, + RadrootsTradeMutationRecordV1, RadrootsTradeNegotiationStateV1, + RadrootsTradePaymentStateV1, RadrootsTradePrivateTermsEvidenceV1, + RadrootsTradePrivateTermsStateV1, RadrootsTradeProjectionV1, RadrootsTradeReducerIssueV1, + RadrootsTradeReductionInputV1, reduce_trade_records, + }; +} + +pub use model::RadrootsTradeProjectionV1 as Projection; +pub use reducer::{ + RadrootsTradeReducerIssueV1 as ReducerIssue, RadrootsTradeReductionInputV1 as ReductionInput, +}; diff --git a/crates/trade/src/model.rs b/crates/trade/src/model.rs @@ -1,5 +1,11 @@ //! Native trade-domain models and validated value types. +pub use crate::trade_contract_v1::{ + RadrootsTradeAgreementClaimV1, RadrootsTradeAgreementStateV1, RadrootsTradeAttestationStateV1, + RadrootsTradeConflictStateV1, RadrootsTradeFulfillmentStateV1, RadrootsTradeNegotiationStateV1, + RadrootsTradePaymentStateV1, RadrootsTradePrivateTermsStateV1, RadrootsTradeProjectionV1, +}; + #[cfg(not(feature = "std"))] use alloc::string::{String, ToString}; #[cfg(feature = "std")] diff --git a/crates/trade/src/reducer.rs b/crates/trade/src/reducer.rs @@ -1 +1,6 @@ //! Deterministic trade reduction and conflict analysis. + +pub use crate::trade_contract_v1::{ + RADROOTS_TRADE_REDUCER_CONTRACT_ID, RADROOTS_TRADE_REDUCER_VERSION, + RadrootsTradeReducerIssueV1, RadrootsTradeReductionInputV1, reduce_trade_records, +}; diff --git a/crates/trade/src/workflow.rs b/crates/trade/src/workflow.rs @@ -134,12 +134,12 @@ pub enum RadrootsTradePaymentStateV1 { #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Clone, Debug, PartialEq, Eq)] pub struct RadrootsTradeReductionInputV1 { - pub trade_id: TradeId, - pub mutations: Vec<RadrootsTradeMutationRecordV1>, - pub private_terms: Vec<RadrootsTradePrivateTermsEvidenceV1>, - pub attestations: Vec<RadrootsTradeAttestationRecordV1>, - pub evidence_state: RadrootsTradeEvidenceStateV1, - pub observed_at_unix_s: Option<u64>, + trade_id: TradeId, + mutations: Vec<RadrootsTradeMutationRecordV1>, + private_terms: Vec<RadrootsTradePrivateTermsEvidenceV1>, + attestations: Vec<RadrootsTradeAttestationRecordV1>, + evidence_state: RadrootsTradeEvidenceStateV1, + observed_at_unix_s: Option<u64>, } impl RadrootsTradeReductionInputV1 { @@ -153,13 +153,93 @@ impl RadrootsTradeReductionInputV1 { observed_at_unix_s: None, } } + + #[must_use] + pub fn with_mutations(mut self, mutations: Vec<RadrootsTradeMutationRecordV1>) -> Self { + self.mutations = mutations; + self + } + + #[must_use] + pub fn with_private_terms( + mut self, + private_terms: Vec<RadrootsTradePrivateTermsEvidenceV1>, + ) -> Self { + self.private_terms = private_terms; + self + } + + #[must_use] + pub fn with_attestations( + mut self, + attestations: Vec<RadrootsTradeAttestationRecordV1>, + ) -> Self { + self.attestations = attestations; + self + } + + #[must_use] + pub fn with_evidence_state(mut self, evidence_state: RadrootsTradeEvidenceStateV1) -> Self { + self.evidence_state = evidence_state; + self + } + + #[must_use] + pub fn with_observed_at_unix_s(mut self, observed_at_unix_s: Option<u64>) -> Self { + self.observed_at_unix_s = observed_at_unix_s; + self + } + + pub const fn trade_id(&self) -> &TradeId { + &self.trade_id + } + + pub fn mutations(&self) -> &[RadrootsTradeMutationRecordV1] { + &self.mutations + } + + pub fn private_terms(&self) -> &[RadrootsTradePrivateTermsEvidenceV1] { + &self.private_terms + } + + pub fn attestations(&self) -> &[RadrootsTradeAttestationRecordV1] { + &self.attestations + } + + pub const fn evidence_state(&self) -> RadrootsTradeEvidenceStateV1 { + self.evidence_state + } + + pub const fn observed_at_unix_s(&self) -> Option<u64> { + self.observed_at_unix_s + } } #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Clone, Debug, PartialEq, Eq)] pub struct RadrootsTradeMutationRecordV1 { - pub transport_event_id: Option<EventId>, - pub mutation: TradeMutationEnvelopeV1, + transport_event_id: Option<EventId>, + mutation: TradeMutationEnvelopeV1, +} + +impl RadrootsTradeMutationRecordV1 { + pub const fn new( + transport_event_id: Option<EventId>, + mutation: TradeMutationEnvelopeV1, + ) -> Self { + Self { + transport_event_id, + mutation, + } + } + + pub const fn transport_event_id(&self) -> Option<&EventId> { + self.transport_event_id.as_ref() + } + + pub const fn mutation(&self) -> &TradeMutationEnvelopeV1 { + &self.mutation + } } #[cfg_attr(feature = "dto-bindgen", derive(dto_bindgen::Dto))] @@ -168,8 +248,25 @@ pub struct RadrootsTradeMutationRecordV1 { #[derive(Clone, Debug, PartialEq, Eq)] pub struct RadrootsTradePrivateTermsEvidenceV1 { #[cfg_attr(feature = "dto-bindgen", dto(as = "string"))] - pub candidate_id: CandidateId, - pub state: RadrootsTradePrivateTermsStateV1, + candidate_id: CandidateId, + state: RadrootsTradePrivateTermsStateV1, +} + +impl RadrootsTradePrivateTermsEvidenceV1 { + pub const fn new(candidate_id: CandidateId, state: RadrootsTradePrivateTermsStateV1) -> Self { + Self { + candidate_id, + state, + } + } + + pub const fn candidate_id(&self) -> &CandidateId { + &self.candidate_id + } + + pub const fn state(&self) -> RadrootsTradePrivateTermsStateV1 { + self.state + } } #[cfg_attr(feature = "dto-bindgen", derive(dto_bindgen::Dto))] @@ -187,10 +284,36 @@ pub enum RadrootsTradeAttestationResultV1 { #[derive(Clone, Debug, PartialEq, Eq)] pub struct RadrootsTradeAttestationRecordV1 { #[cfg_attr(feature = "dto-bindgen", dto(as = "string"))] - pub event_id: EventId, + event_id: EventId, #[cfg_attr(feature = "dto-bindgen", dto(as = "string"))] - pub claim_mutation_id: MutationId, - pub result: RadrootsTradeAttestationResultV1, + claim_mutation_id: MutationId, + result: RadrootsTradeAttestationResultV1, +} + +impl RadrootsTradeAttestationRecordV1 { + pub const fn new( + event_id: EventId, + claim_mutation_id: MutationId, + result: RadrootsTradeAttestationResultV1, + ) -> Self { + Self { + event_id, + claim_mutation_id, + result, + } + } + + pub const fn event_id(&self) -> &EventId { + &self.event_id + } + + pub const fn claim_mutation_id(&self) -> &MutationId { + &self.claim_mutation_id + } + + pub const fn result(&self) -> RadrootsTradeAttestationResultV1 { + self.result + } } #[cfg_attr(feature = "dto-bindgen", derive(dto_bindgen::Dto))] @@ -198,46 +321,46 @@ pub struct RadrootsTradeAttestationRecordV1 { #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Clone, Debug, PartialEq, Eq)] pub struct RadrootsTradeProjectionV1 { - pub reducer_contract_id: String, - pub reducer_version: u16, + reducer_contract_id: String, + reducer_version: u16, #[cfg_attr(feature = "dto-bindgen", dto(as = "string"))] - pub trade_id: TradeId, + trade_id: TradeId, #[cfg_attr(feature = "dto-bindgen", dto(as = "string"))] - pub root_mutation_id: Option<MutationId>, + root_mutation_id: Option<MutationId>, #[cfg_attr(feature = "dto-bindgen", dto(as = "string"))] - pub buyer_pubkey: Option<PublicKey>, + buyer_pubkey: Option<PublicKey>, #[cfg_attr(feature = "dto-bindgen", dto(as = "string"))] - pub seller_pubkey: Option<PublicKey>, + seller_pubkey: Option<PublicKey>, #[cfg_attr(feature = "dto-bindgen", dto(as = "string"))] - pub farm_id: Option<DTag>, - pub negotiation_state: RadrootsTradeNegotiationStateV1, - pub agreement_state: RadrootsTradeAgreementStateV1, - pub evidence_state: RadrootsTradeEvidenceStateV1, - pub conflict_state: RadrootsTradeConflictStateV1, - pub private_terms_state: RadrootsTradePrivateTermsStateV1, - pub attestation_state: RadrootsTradeAttestationStateV1, - pub fulfillment_state: RadrootsTradeFulfillmentStateV1, - pub payment_state: RadrootsTradePaymentStateV1, + farm_id: Option<DTag>, + negotiation_state: RadrootsTradeNegotiationStateV1, + agreement_state: RadrootsTradeAgreementStateV1, + evidence_state: RadrootsTradeEvidenceStateV1, + conflict_state: RadrootsTradeConflictStateV1, + private_terms_state: RadrootsTradePrivateTermsStateV1, + attestation_state: RadrootsTradeAttestationStateV1, + fulfillment_state: RadrootsTradeFulfillmentStateV1, + payment_state: RadrootsTradePaymentStateV1, #[cfg_attr(feature = "dto-bindgen", dto(ts(type = "Array<string>")))] - pub candidate_heads: Vec<MutationId>, - pub agreement_claims: Vec<RadrootsTradeAgreementClaimV1>, + candidate_heads: Vec<MutationId>, + agreement_claims: Vec<RadrootsTradeAgreementClaimV1>, #[cfg_attr(feature = "dto-bindgen", dto(ts(type = "Array<string>")))] - pub active_agreement_claim_ids: Vec<MutationId>, + active_agreement_claim_ids: Vec<MutationId>, #[cfg_attr(feature = "dto-bindgen", dto(ts(type = "Array<string>")))] - pub contested_claim_ids: Vec<MutationId>, + contested_claim_ids: Vec<MutationId>, #[cfg_attr(feature = "dto-bindgen", dto(ts(type = "Array<string>")))] - pub cancelled_claim_ids: Vec<MutationId>, + cancelled_claim_ids: Vec<MutationId>, #[cfg_attr(feature = "dto-bindgen", dto(ts(type = "Array<string>")))] - pub declined_candidate_ids: Vec<CandidateId>, + declined_candidate_ids: Vec<CandidateId>, #[cfg_attr(feature = "dto-bindgen", dto(ts(type = "Array<string>")))] - pub missing_parent_ids: Vec<MutationId>, + missing_parent_ids: Vec<MutationId>, #[cfg_attr(feature = "dto-bindgen", dto(ts(type = "Array<string>")))] - pub missing_proposal_ids: Vec<MutationId>, + missing_proposal_ids: Vec<MutationId>, #[cfg_attr(feature = "dto-bindgen", dto(ts(type = "Array<string>")))] - pub unsupported_mutation_ids: Vec<MutationId>, - pub issues: Vec<RadrootsTradeReducerIssueV1>, - pub attestations: Vec<RadrootsTradeAttestationRecordV1>, - pub projection_digest: String, + unsupported_mutation_ids: Vec<MutationId>, + issues: Vec<RadrootsTradeReducerIssueV1>, + attestations: Vec<RadrootsTradeAttestationRecordV1>, + projection_digest: String, } impl RadrootsTradeProjectionV1 { @@ -297,6 +420,114 @@ impl RadrootsTradeProjectionV1 { self.attestations.sort_by_key(|left| left.event_id); self.projection_digest = projection_digest(self); } + + pub fn reducer_contract_id(&self) -> &str { + &self.reducer_contract_id + } + + pub const fn reducer_version(&self) -> u16 { + self.reducer_version + } + + pub const fn trade_id(&self) -> &TradeId { + &self.trade_id + } + + pub const fn root_mutation_id(&self) -> Option<&MutationId> { + self.root_mutation_id.as_ref() + } + + pub const fn buyer_pubkey(&self) -> Option<&PublicKey> { + self.buyer_pubkey.as_ref() + } + + pub const fn seller_pubkey(&self) -> Option<&PublicKey> { + self.seller_pubkey.as_ref() + } + + pub const fn farm_id(&self) -> Option<&DTag> { + self.farm_id.as_ref() + } + + pub const fn negotiation_state(&self) -> RadrootsTradeNegotiationStateV1 { + self.negotiation_state + } + + pub const fn agreement_state(&self) -> RadrootsTradeAgreementStateV1 { + self.agreement_state + } + + pub const fn evidence_state(&self) -> RadrootsTradeEvidenceStateV1 { + self.evidence_state + } + + pub const fn conflict_state(&self) -> RadrootsTradeConflictStateV1 { + self.conflict_state + } + + pub const fn private_terms_state(&self) -> RadrootsTradePrivateTermsStateV1 { + self.private_terms_state + } + + pub const fn attestation_state(&self) -> RadrootsTradeAttestationStateV1 { + self.attestation_state + } + + pub const fn fulfillment_state(&self) -> RadrootsTradeFulfillmentStateV1 { + self.fulfillment_state + } + + pub const fn payment_state(&self) -> RadrootsTradePaymentStateV1 { + self.payment_state + } + + pub fn candidate_heads(&self) -> &[MutationId] { + &self.candidate_heads + } + + pub fn agreement_claims(&self) -> &[RadrootsTradeAgreementClaimV1] { + &self.agreement_claims + } + + pub fn active_agreement_claim_ids(&self) -> &[MutationId] { + &self.active_agreement_claim_ids + } + + pub fn contested_claim_ids(&self) -> &[MutationId] { + &self.contested_claim_ids + } + + pub fn cancelled_claim_ids(&self) -> &[MutationId] { + &self.cancelled_claim_ids + } + + pub fn declined_candidate_ids(&self) -> &[CandidateId] { + &self.declined_candidate_ids + } + + pub fn missing_parent_ids(&self) -> &[MutationId] { + &self.missing_parent_ids + } + + pub fn missing_proposal_ids(&self) -> &[MutationId] { + &self.missing_proposal_ids + } + + pub fn unsupported_mutation_ids(&self) -> &[MutationId] { + &self.unsupported_mutation_ids + } + + pub fn issues(&self) -> &[RadrootsTradeReducerIssueV1] { + &self.issues + } + + pub fn attestations(&self) -> &[RadrootsTradeAttestationRecordV1] { + &self.attestations + } + + pub fn projection_digest(&self) -> &str { + &self.projection_digest + } } #[cfg_attr(feature = "dto-bindgen", derive(dto_bindgen::Dto))] @@ -305,16 +536,42 @@ impl RadrootsTradeProjectionV1 { #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct RadrootsTradeAgreementClaimV1 { #[cfg_attr(feature = "dto-bindgen", dto(as = "string"))] - pub claim_mutation_id: MutationId, + claim_mutation_id: MutationId, #[cfg_attr(feature = "dto-bindgen", dto(as = "string"))] - pub proposal_mutation_id: MutationId, + proposal_mutation_id: MutationId, #[cfg_attr(feature = "dto-bindgen", dto(as = "string"))] - pub candidate_id: CandidateId, + candidate_id: CandidateId, #[cfg_attr(feature = "dto-bindgen", dto(as = "string"))] - pub candidate_author_pubkey: PublicKey, + candidate_author_pubkey: PublicKey, #[cfg_attr(feature = "dto-bindgen", dto(as = "string"))] - pub accepted_by_pubkey: PublicKey, - pub reservation_commitment: String, + accepted_by_pubkey: PublicKey, + reservation_commitment: String, +} + +impl RadrootsTradeAgreementClaimV1 { + pub const fn claim_mutation_id(&self) -> &MutationId { + &self.claim_mutation_id + } + + pub const fn proposal_mutation_id(&self) -> &MutationId { + &self.proposal_mutation_id + } + + pub const fn candidate_id(&self) -> &CandidateId { + &self.candidate_id + } + + pub const fn candidate_author_pubkey(&self) -> &PublicKey { + &self.candidate_author_pubkey + } + + pub const fn accepted_by_pubkey(&self) -> &PublicKey { + &self.accepted_by_pubkey + } + + pub fn reservation_commitment(&self) -> &str { + &self.reservation_commitment + } } #[cfg_attr(feature = "dto-bindgen", derive(dto_bindgen::Dto))] diff --git a/crates/trade/tests/package_boundary.rs b/crates/trade/tests/package_boundary.rs @@ -8,6 +8,7 @@ const DRAFT: &str = include_str!("../src/operational_listing/draft.rs"); const IDENTITY: &str = include_str!("../src/identity.rs"); const MODEL: &str = include_str!("../src/model.rs"); const ROOT: &str = include_str!("../src/lib.rs"); +const TRADE_CONTRACT: &str = include_str!("../src/workflow.rs"); const PACKAGE_TIERS: &str = include_str!("../../../contracts/releases/package_tiers.toml"); #[test] @@ -31,7 +32,7 @@ fn crate_root_declares_every_approved_module() { let declared = root_declarations("pub mod "); for module in ["evidence", "model", "reducer", "validation", "workflow"] { assert!( - declared.contains(module), + declared.contains(module) || ROOT.contains(&format!("pub mod {module} {{")), "missing approved module {module}" ); } @@ -101,6 +102,55 @@ fn trade_feature_graph_has_no_persistence_or_sql_boundary() { assert!(!MANIFEST.contains("runtime-tokio")); } +#[test] +fn trade_model_reducer_and_evidence_have_final_public_owners() { + use radroots_trade::{Projection, ReducerIssue, ReductionInput}; + use radroots_trade::{evidence::RadrootsTradeEvidenceStateV1, reducer::reduce_trade_records}; + + let trade_id = radroots_event::trade::TradeId::parse("22".repeat(16)) + .expect("canonical protocol trade id"); + let input = ReductionInput::new(trade_id) + .with_evidence_state(RadrootsTradeEvidenceStateV1::Complete) + .with_mutations(Vec::new()) + .with_private_terms(Vec::new()) + .with_attestations(Vec::new()) + .with_observed_at_unix_s(Some(42)); + + assert_eq!(input.trade_id(), &trade_id); + assert_eq!( + input.evidence_state(), + RadrootsTradeEvidenceStateV1::Complete + ); + assert_eq!(input.observed_at_unix_s(), Some(42)); + assert!(input.mutations().is_empty()); + + let projection: Projection = reduce_trade_records(input.clone()); + let _: &[ReducerIssue] = projection.issues(); + assert_eq!(projection.trade_id(), &trade_id); + assert_eq!( + projection.evidence_state(), + RadrootsTradeEvidenceStateV1::Missing + ); + + let serialized = serde_json::to_value(input).expect("serialize reduction input"); + assert_eq!(serialized["trade_id"], trade_id.to_string()); + assert_eq!(serialized["observed_at_unix_s"], 42); + assert_eq!(serialized["mutations"], serde_json::json!([])); + + for declaration in [ + "pub trade_id:", + "pub mutations:", + "pub projection_digest:", + "pub candidate_id:", + "pub claim_mutation_id:", + ] { + assert!( + !TRADE_CONTRACT.contains(declaration), + "native trade contract field must remain private: {declaration}" + ); + } +} + fn table_keys<'a>(manifest: &'a str, heading: &str) -> BTreeSet<&'a str> { let Some((_, table)) = manifest.split_once(heading) else { return BTreeSet::new();