lib

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

trade.rs (65594B)


      1 #![forbid(unsafe_code)]
      2 
      3 #[path = "order.rs"]
      4 pub mod order;
      5 #[path = "order_economics.rs"]
      6 pub mod order_economics;
      7 #[path = "trade_validation.rs"]
      8 pub mod validation;
      9 
     10 #[cfg(all(not(feature = "std"), feature = "serde"))]
     11 use alloc::{collections::BTreeMap, format, string::ToString};
     12 #[cfg(not(feature = "std"))]
     13 use alloc::{string::String, vec::Vec};
     14 #[cfg(all(feature = "std", feature = "serde"))]
     15 use std::{collections::BTreeMap, string::ToString};
     16 #[cfg(feature = "std")]
     17 use std::{string::String, vec::Vec};
     18 
     19 use core::fmt;
     20 
     21 use crate::envelope::kind::{
     22     KIND_TRADE_CANCELLATION, KIND_TRADE_DECISION, KIND_TRADE_PROPOSAL,
     23     KIND_TRADE_REVISION_DECISION, KIND_TRADE_REVISION_PROPOSAL,
     24 };
     25 pub use crate::id::{CandidateId, MutationId, TradeId};
     26 use crate::id::{ClassifiedListingAddress, DTag, EventId, InventoryBinId, ParseError};
     27 use radroots_identity::PublicKey;
     28 #[cfg(feature = "serde")]
     29 use serde::{
     30     Deserialize, Deserializer, Serialize,
     31     de::{Error as _, MapAccess, SeqAccess, Visitor},
     32 };
     33 #[cfg(feature = "serde")]
     34 use serde_json::{Map, Number, Value};
     35 #[cfg(feature = "serde")]
     36 use sha2::{Digest, Sha256};
     37 
     38 pub const RADROOTS_TRADE_SCHEMA_VERSION: u16 = 1;
     39 pub const RADROOTS_TRADE_MUTATION_DOMAIN: &[u8] = b"radroots:trade-mutation:v1\0";
     40 pub const RADROOTS_TRADE_CANDIDATE_DOMAIN: &[u8] = b"radroots:trade-candidate:v1\0";
     41 pub const RADROOTS_TRADE_PROPOSAL_CONTRACT_ID: &str = "radroots.trade.proposal.v1";
     42 pub const RADROOTS_TRADE_DECISION_CONTRACT_ID: &str = "radroots.trade.decision.v1";
     43 pub const RADROOTS_TRADE_REVISION_PROPOSAL_CONTRACT_ID: &str =
     44     "radroots.trade.revision_proposal.v1";
     45 pub const RADROOTS_TRADE_REVISION_DECISION_CONTRACT_ID: &str =
     46     "radroots.trade.revision_decision.v1";
     47 pub const RADROOTS_TRADE_CANCELLATION_CONTRACT_ID: &str = "radroots.trade.cancellation.v1";
     48 pub const RADROOTS_TRADE_SELLER_RESERVATION_ASSERTION_CONTRACT_ID: &str =
     49     "radroots.trade.seller_reservation_assertion.v1";
     50 pub const RADROOTS_TRADE_MUTATION_CONTRACT_IDS: [&str; 5] = [
     51     RADROOTS_TRADE_PROPOSAL_CONTRACT_ID,
     52     RADROOTS_TRADE_DECISION_CONTRACT_ID,
     53     RADROOTS_TRADE_REVISION_PROPOSAL_CONTRACT_ID,
     54     RADROOTS_TRADE_REVISION_DECISION_CONTRACT_ID,
     55     RADROOTS_TRADE_CANCELLATION_CONTRACT_ID,
     56 ];
     57 pub const RADROOTS_TRADE_MAX_PUBLIC_CONTENT_BYTES: usize = 64 * 1024;
     58 pub const RADROOTS_TRADE_MAX_ACTIVE_LINES: usize = 64;
     59 pub const RADROOTS_TRADE_MAX_ADJUSTMENTS: usize = 128;
     60 pub const RADROOTS_TRADE_MAX_PARENT_MUTATIONS: usize = 4;
     61 pub const RADROOTS_TRADE_MAX_PRIVATE_ARTIFACT_BYTES: usize = 64 * 1024;
     62 
     63 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
     64 pub enum TradeMutationKindV1 {
     65     Proposal,
     66     Decision,
     67     RevisionProposal,
     68     RevisionDecision,
     69     Cancellation,
     70 }
     71 
     72 impl TradeMutationKindV1 {
     73     pub const fn contract_id(self) -> &'static str {
     74         match self {
     75             Self::Proposal => RADROOTS_TRADE_PROPOSAL_CONTRACT_ID,
     76             Self::Decision => RADROOTS_TRADE_DECISION_CONTRACT_ID,
     77             Self::RevisionProposal => RADROOTS_TRADE_REVISION_PROPOSAL_CONTRACT_ID,
     78             Self::RevisionDecision => RADROOTS_TRADE_REVISION_DECISION_CONTRACT_ID,
     79             Self::Cancellation => RADROOTS_TRADE_CANCELLATION_CONTRACT_ID,
     80         }
     81     }
     82 
     83     pub const fn nostr_kind(self) -> u32 {
     84         match self {
     85             Self::Proposal => KIND_TRADE_PROPOSAL,
     86             Self::Decision => KIND_TRADE_DECISION,
     87             Self::RevisionProposal => KIND_TRADE_REVISION_PROPOSAL,
     88             Self::RevisionDecision => KIND_TRADE_REVISION_DECISION,
     89             Self::Cancellation => KIND_TRADE_CANCELLATION,
     90         }
     91     }
     92 }
     93 
     94 #[derive(Clone, Debug, PartialEq, Eq)]
     95 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
     96 pub struct TradeMutationEnvelopeV1 {
     97     pub mutation_id: Option<MutationId>,
     98     pub contract_id: String,
     99     pub schema_version: u16,
    100     pub trade_id: TradeId,
    101     pub root_mutation_id: Option<MutationId>,
    102     pub buyer_pubkey: PublicKey,
    103     pub seller_pubkey: PublicKey,
    104     pub farm_id: DTag,
    105     pub parent_mutation_ids: Vec<MutationId>,
    106     pub author_pubkey: PublicKey,
    107     pub counterparty_pubkey: PublicKey,
    108     pub authored_at_unix_s: u64,
    109     pub body: TradeMutationBodyV1,
    110 }
    111 
    112 impl TradeMutationEnvelopeV1 {
    113     pub fn mutation_kind(&self) -> TradeMutationKindV1 {
    114         self.body.mutation_kind()
    115     }
    116 
    117     pub fn validate(&self) -> Result<(), TradeProtocolError> {
    118         if self.schema_version != RADROOTS_TRADE_SCHEMA_VERSION {
    119             return Err(TradeProtocolError::InvalidSchemaVersion {
    120                 expected: RADROOTS_TRADE_SCHEMA_VERSION,
    121                 actual: self.schema_version,
    122             });
    123         }
    124         let expected = self.mutation_kind().contract_id();
    125         if self.contract_id != expected {
    126             return Err(TradeProtocolError::ContractMismatch {
    127                 expected,
    128                 actual: self.contract_id.clone(),
    129             });
    130         }
    131         validate_parent_mutation_ids(self.mutation_id.as_ref(), &self.parent_mutation_ids)?;
    132         match self.mutation_kind() {
    133             TradeMutationKindV1::Proposal => {
    134                 if self.root_mutation_id.is_some() || !self.parent_mutation_ids.is_empty() {
    135                     return Err(TradeProtocolError::InvalidInitialParents);
    136                 }
    137             }
    138             _ => {
    139                 if self.root_mutation_id.is_none() || self.parent_mutation_ids.is_empty() {
    140                     return Err(TradeProtocolError::MissingParentMutation);
    141                 }
    142             }
    143         }
    144         self.body.validate()
    145     }
    146 }
    147 
    148 #[derive(Clone, Debug, PartialEq, Eq)]
    149 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
    150 #[cfg_attr(
    151     feature = "serde",
    152     serde(rename_all = "snake_case", tag = "mutation_type")
    153 )]
    154 pub enum TradeMutationBodyV1 {
    155     Proposal {
    156         candidate: TradeCandidateTermsV1,
    157     },
    158     Decision {
    159         proposal_mutation_id: MutationId,
    160         candidate_id: CandidateId,
    161         decision: TradeDecisionV1,
    162     },
    163     RevisionProposal {
    164         candidate: TradeCandidateTermsV1,
    165     },
    166     RevisionDecision {
    167         proposal_mutation_id: MutationId,
    168         candidate_id: CandidateId,
    169         decision: TradeDecisionV1,
    170     },
    171     Cancellation {
    172         target_candidate_id: Option<CandidateId>,
    173         target_claim_mutation_id: Option<MutationId>,
    174         reason: String,
    175     },
    176 }
    177 
    178 impl TradeMutationBodyV1 {
    179     pub const fn mutation_kind(&self) -> TradeMutationKindV1 {
    180         match self {
    181             Self::Proposal { .. } => TradeMutationKindV1::Proposal,
    182             Self::Decision { .. } => TradeMutationKindV1::Decision,
    183             Self::RevisionProposal { .. } => TradeMutationKindV1::RevisionProposal,
    184             Self::RevisionDecision { .. } => TradeMutationKindV1::RevisionDecision,
    185             Self::Cancellation { .. } => TradeMutationKindV1::Cancellation,
    186         }
    187     }
    188 
    189     pub fn validate(&self) -> Result<(), TradeProtocolError> {
    190         match self {
    191             Self::Proposal { candidate } | Self::RevisionProposal { candidate } => {
    192                 candidate.validate()
    193             }
    194             Self::Decision {
    195                 proposal_mutation_id: _,
    196                 candidate_id: _,
    197                 decision,
    198             }
    199             | Self::RevisionDecision {
    200                 proposal_mutation_id: _,
    201                 candidate_id: _,
    202                 decision,
    203             } => decision.validate(),
    204             Self::Cancellation {
    205                 target_candidate_id,
    206                 target_claim_mutation_id,
    207                 reason,
    208             } => {
    209                 if target_candidate_id.is_none() && target_claim_mutation_id.is_none() {
    210                     return Err(TradeProtocolError::MissingCancellationTarget);
    211                 }
    212                 validate_non_empty(reason, "reason")
    213             }
    214         }
    215     }
    216 }
    217 
    218 #[derive(Clone, Debug, PartialEq, Eq)]
    219 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
    220 pub struct TradeCandidateTermsV1 {
    221     pub candidate_id: Option<CandidateId>,
    222     pub schema_version: u16,
    223     pub base_candidate_id: Option<CandidateId>,
    224     pub supersession_intent: Option<String>,
    225     pub buyer_pubkey: PublicKey,
    226     pub seller_pubkey: PublicKey,
    227     pub farm_id: DTag,
    228     pub lines: Vec<TradeCandidateLineV1>,
    229     pub line_tombstones: Vec<TradeLineTombstoneV1>,
    230     pub economics: TradeEconomicsProfileV1,
    231     pub fulfillment: FulfillmentProfileV1,
    232     pub cancellation: TradeCancellationProfileV1,
    233     pub private_terms: Option<TradePrivateTermsRefV1>,
    234     pub proposal_expires_at_unix_s: u64,
    235 }
    236 
    237 impl TradeCandidateTermsV1 {
    238     pub fn validate(&self) -> Result<(), TradeProtocolError> {
    239         if self.schema_version != RADROOTS_TRADE_SCHEMA_VERSION {
    240             return Err(TradeProtocolError::InvalidSchemaVersion {
    241                 expected: RADROOTS_TRADE_SCHEMA_VERSION,
    242                 actual: self.schema_version,
    243             });
    244         }
    245         if self.lines.is_empty() {
    246             return Err(TradeProtocolError::MissingLines);
    247         }
    248         if self.lines.len() > RADROOTS_TRADE_MAX_ACTIVE_LINES {
    249             return Err(TradeProtocolError::TooManyLines {
    250                 max: RADROOTS_TRADE_MAX_ACTIVE_LINES,
    251                 actual: self.lines.len(),
    252             });
    253         }
    254         validate_sorted_unique_by(&self.lines, |line| line.line_id.as_str(), "lines")?;
    255         validate_sorted_unique_by(
    256             &self.line_tombstones,
    257             |line| line.line_id.as_str(),
    258             "line_tombstones",
    259         )?;
    260         for line in &self.lines {
    261             line.validate()?;
    262         }
    263         for tombstone in &self.line_tombstones {
    264             tombstone.validate()?;
    265         }
    266         self.economics.validate()?;
    267         self.fulfillment.validate()?;
    268         self.cancellation.validate()?;
    269         if let Some(private_terms) = &self.private_terms {
    270             private_terms.validate()?;
    271         }
    272         Ok(())
    273     }
    274 }
    275 
    276 #[derive(Clone, Debug, PartialEq, Eq)]
    277 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
    278 pub struct TradeCandidateLineV1 {
    279     pub line_id: DTag,
    280     pub listing_addr: ClassifiedListingAddress,
    281     pub listing_event_id: EventId,
    282     pub listing_snapshot_sha256: String,
    283     pub product_id: String,
    284     pub option_id: Option<String>,
    285     pub bin_id: InventoryBinId,
    286     pub quantity_mantissa: String,
    287     pub quantity_scale: u8,
    288     pub unit_code: String,
    289     pub unit_profile: String,
    290     pub unit_price_mantissa: String,
    291     pub currency_code: String,
    292     pub line_subtotal_mantissa: String,
    293     pub replaces_line_id: Option<DTag>,
    294 }
    295 
    296 impl TradeCandidateLineV1 {
    297     pub fn validate(&self) -> Result<(), TradeProtocolError> {
    298         validate_sha256_hex(&self.listing_snapshot_sha256, "listing_snapshot_sha256")?;
    299         validate_non_empty(&self.product_id, "product_id")?;
    300         if let Some(option_id) = &self.option_id {
    301             validate_non_empty(option_id, "option_id")?;
    302         }
    303         validate_decimal_string(&self.quantity_mantissa, "quantity_mantissa")?;
    304         validate_non_empty(&self.unit_code, "unit_code")?;
    305         validate_non_empty(&self.unit_profile, "unit_profile")?;
    306         validate_decimal_string(&self.unit_price_mantissa, "unit_price_mantissa")?;
    307         validate_non_empty(&self.currency_code, "currency_code")?;
    308         validate_decimal_string(&self.line_subtotal_mantissa, "line_subtotal_mantissa")
    309     }
    310 }
    311 
    312 #[derive(Clone, Debug, PartialEq, Eq)]
    313 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
    314 pub struct TradeLineTombstoneV1 {
    315     pub line_id: DTag,
    316     pub reason: String,
    317 }
    318 
    319 impl TradeLineTombstoneV1 {
    320     pub fn validate(&self) -> Result<(), TradeProtocolError> {
    321         validate_non_empty(&self.reason, "line_tombstone.reason")
    322     }
    323 }
    324 
    325 #[derive(Clone, Debug, PartialEq, Eq)]
    326 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
    327 pub struct TradeEconomicsProfileV1 {
    328     pub profile_id: String,
    329     pub currency_code: String,
    330     pub currency_exponent: u8,
    331     pub rounding_profile: String,
    332     pub subtotal_mantissa: String,
    333     pub discount_total_mantissa: String,
    334     pub adjustment_total_mantissa: String,
    335     pub total_mantissa: String,
    336     pub adjustments: Vec<TradeEconomicAdjustmentV1>,
    337 }
    338 
    339 impl TradeEconomicsProfileV1 {
    340     pub fn validate(&self) -> Result<(), TradeProtocolError> {
    341         validate_non_empty(&self.profile_id, "economics.profile_id")?;
    342         validate_non_empty(&self.currency_code, "economics.currency_code")?;
    343         validate_non_empty(&self.rounding_profile, "economics.rounding_profile")?;
    344         validate_decimal_string(&self.subtotal_mantissa, "economics.subtotal_mantissa")?;
    345         validate_decimal_string(
    346             &self.discount_total_mantissa,
    347             "economics.discount_total_mantissa",
    348         )?;
    349         validate_decimal_string(
    350             &self.adjustment_total_mantissa,
    351             "economics.adjustment_total_mantissa",
    352         )?;
    353         validate_decimal_string(&self.total_mantissa, "economics.total_mantissa")?;
    354         if self.adjustments.len() > RADROOTS_TRADE_MAX_ADJUSTMENTS {
    355             return Err(TradeProtocolError::TooManyAdjustments {
    356                 max: RADROOTS_TRADE_MAX_ADJUSTMENTS,
    357                 actual: self.adjustments.len(),
    358             });
    359         }
    360         validate_sorted_unique_by(
    361             &self.adjustments,
    362             |line| line.adjustment_id.as_str(),
    363             "adjustments",
    364         )?;
    365         for adjustment in &self.adjustments {
    366             adjustment.validate()?;
    367         }
    368         Ok(())
    369     }
    370 }
    371 
    372 #[derive(Clone, Debug, PartialEq, Eq)]
    373 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
    374 pub struct TradeEconomicAdjustmentV1 {
    375     pub adjustment_id: DTag,
    376     pub actor: String,
    377     pub effect: String,
    378     pub amount_mantissa: String,
    379     pub reason: String,
    380 }
    381 
    382 impl TradeEconomicAdjustmentV1 {
    383     pub fn validate(&self) -> Result<(), TradeProtocolError> {
    384         validate_non_empty(&self.actor, "adjustment.actor")?;
    385         validate_non_empty(&self.effect, "adjustment.effect")?;
    386         validate_decimal_string(&self.amount_mantissa, "adjustment.amount_mantissa")?;
    387         validate_non_empty(&self.reason, "adjustment.reason")
    388     }
    389 }
    390 
    391 #[derive(Clone, Debug, PartialEq, Eq)]
    392 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
    393 pub struct FulfillmentProfileV1 {
    394     pub profile_id: String,
    395     pub method: String,
    396     pub starts_at_unix_s: u64,
    397     pub ends_at_unix_s: u64,
    398     pub timezone: String,
    399     pub utc_offset_seconds: i32,
    400     pub fold: u8,
    401     pub location_class: String,
    402     pub requires_private_terms: bool,
    403 }
    404 
    405 impl FulfillmentProfileV1 {
    406     pub fn validate(&self) -> Result<(), TradeProtocolError> {
    407         validate_non_empty(&self.profile_id, "fulfillment.profile_id")?;
    408         validate_non_empty(&self.method, "fulfillment.method")?;
    409         validate_non_empty(&self.timezone, "fulfillment.timezone")?;
    410         validate_non_empty(&self.location_class, "fulfillment.location_class")?;
    411         if self.ends_at_unix_s <= self.starts_at_unix_s {
    412             return Err(TradeProtocolError::InvalidTimeRange);
    413         }
    414         if self.fold > 1 {
    415             return Err(TradeProtocolError::InvalidField("fulfillment.fold"));
    416         }
    417         Ok(())
    418     }
    419 }
    420 
    421 #[derive(Clone, Debug, PartialEq, Eq)]
    422 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
    423 pub struct TradeCancellationProfileV1 {
    424     pub profile_id: String,
    425     pub buyer_pre_agreement: bool,
    426     pub post_agreement_cutoff_unix_s: Option<u64>,
    427 }
    428 
    429 impl TradeCancellationProfileV1 {
    430     pub fn validate(&self) -> Result<(), TradeProtocolError> {
    431         validate_non_empty(&self.profile_id, "cancellation.profile_id")
    432     }
    433 }
    434 
    435 #[derive(Clone, Debug, PartialEq, Eq)]
    436 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
    437 pub struct TradePrivateTermsRefV1 {
    438     pub artifact_id: String,
    439     pub schema_id: String,
    440     pub ciphertext_commitment: String,
    441     pub required_acknowledgement: bool,
    442 }
    443 
    444 impl TradePrivateTermsRefV1 {
    445     pub fn validate(&self) -> Result<(), TradeProtocolError> {
    446         validate_non_empty(&self.artifact_id, "private_terms.artifact_id")?;
    447         validate_non_empty(&self.schema_id, "private_terms.schema_id")?;
    448         validate_sha256_hex(
    449             &self.ciphertext_commitment,
    450             "private_terms.ciphertext_commitment",
    451         )
    452     }
    453 }
    454 
    455 #[derive(Clone, Debug, PartialEq, Eq)]
    456 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
    457 pub struct SellerReservationAssertionV1 {
    458     pub reservation_id: DTag,
    459     pub inventory_authority_id: PublicKey,
    460     pub inventory_epoch: u64,
    461     pub candidate_id: CandidateId,
    462     pub commitments: Vec<SellerReservationLineV1>,
    463     pub reservation_expires_at_unix_s: u64,
    464     pub assertion_commitment: String,
    465 }
    466 
    467 impl SellerReservationAssertionV1 {
    468     pub fn validate(&self) -> Result<(), TradeProtocolError> {
    469         if self.commitments.is_empty() {
    470             return Err(TradeProtocolError::MissingReservationCommitments);
    471         }
    472         validate_sorted_unique_by(
    473             &self.commitments,
    474             |line| line.line_id.as_str(),
    475             "reservation.commitments",
    476         )?;
    477         for commitment in &self.commitments {
    478             commitment.validate()?;
    479         }
    480         validate_sha256_hex(
    481             &self.assertion_commitment,
    482             "reservation.assertion_commitment",
    483         )
    484     }
    485 }
    486 
    487 #[derive(Clone, Debug, PartialEq, Eq)]
    488 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
    489 pub struct SellerReservationLineV1 {
    490     pub line_id: DTag,
    491     pub bin_id: InventoryBinId,
    492     pub quantity_mantissa: String,
    493     pub quantity_scale: u8,
    494     pub unit_code: String,
    495 }
    496 
    497 impl SellerReservationLineV1 {
    498     pub fn validate(&self) -> Result<(), TradeProtocolError> {
    499         validate_decimal_string(
    500             &self.quantity_mantissa,
    501             "reservation.line.quantity_mantissa",
    502         )?;
    503         validate_non_empty(&self.unit_code, "reservation.line.unit_code")
    504     }
    505 }
    506 
    507 #[derive(Clone, Debug, PartialEq, Eq)]
    508 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
    509 #[cfg_attr(feature = "serde", serde(rename_all = "snake_case", tag = "decision"))]
    510 pub enum TradeDecisionV1 {
    511     Accepted {
    512         reservation_assertion: Option<SellerReservationAssertionV1>,
    513     },
    514     Declined {
    515         reason: String,
    516     },
    517 }
    518 
    519 impl TradeDecisionV1 {
    520     pub fn validate(&self) -> Result<(), TradeProtocolError> {
    521         match self {
    522             Self::Accepted {
    523                 reservation_assertion,
    524             } => {
    525                 if let Some(assertion) = reservation_assertion {
    526                     assertion.validate()?;
    527                 }
    528                 Ok(())
    529             }
    530             Self::Declined { reason } => validate_non_empty(reason, "decision.reason"),
    531         }
    532     }
    533 }
    534 
    535 #[derive(Clone, Debug, PartialEq, Eq)]
    536 pub struct TradeCanonicalMutationV1 {
    537     pub mutation_id: MutationId,
    538     pub envelope: TradeMutationEnvelopeV1,
    539     pub content: String,
    540 }
    541 
    542 #[derive(Clone, Debug, PartialEq, Eq)]
    543 pub enum TradeProtocolError {
    544     InvalidSchemaVersion {
    545         expected: u16,
    546         actual: u16,
    547     },
    548     ContractMismatch {
    549         expected: &'static str,
    550         actual: String,
    551     },
    552     InvalidInitialParents,
    553     MissingParentMutation,
    554     TooManyParents {
    555         max: usize,
    556         actual: usize,
    557     },
    558     UnsortedParents,
    559     DuplicateParent,
    560     SelfParent,
    561     MissingLines,
    562     TooManyLines {
    563         max: usize,
    564         actual: usize,
    565     },
    566     TooManyAdjustments {
    567         max: usize,
    568         actual: usize,
    569     },
    570     DuplicateKey(String),
    571     InvalidJson(String),
    572     NonCanonicalJson,
    573     UnsupportedNumber,
    574     ContentTooLarge {
    575         max: usize,
    576         actual: usize,
    577     },
    578     EmptyField(&'static str),
    579     InvalidField(&'static str),
    580     InvalidIdentifier {
    581         field: &'static str,
    582         error: ParseError,
    583     },
    584     InvalidTimeRange,
    585     MissingReservationCommitments,
    586     MissingCancellationTarget,
    587     CandidateIdMismatch {
    588         declared: String,
    589         computed: String,
    590     },
    591     MutationIdMismatch {
    592         declared: String,
    593         computed: String,
    594     },
    595 }
    596 
    597 impl fmt::Display for TradeProtocolError {
    598     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    599         match self {
    600             Self::InvalidSchemaVersion { expected, actual } => {
    601                 write!(f, "trade schema version {actual} must be {expected}")
    602             }
    603             Self::ContractMismatch { expected, actual } => {
    604                 write!(f, "trade contract {actual} must be {expected}")
    605             }
    606             Self::InvalidInitialParents => {
    607                 write!(f, "initial trade proposal must not have parents")
    608             }
    609             Self::MissingParentMutation => write!(f, "trade mutation requires a parent mutation"),
    610             Self::TooManyParents { max, actual } => {
    611                 write!(f, "trade mutation has {actual} parents; max is {max}")
    612             }
    613             Self::UnsortedParents => write!(f, "trade mutation parents must be sorted"),
    614             Self::DuplicateParent => write!(f, "trade mutation parents must be unique"),
    615             Self::SelfParent => write!(f, "trade mutation cannot reference itself as a parent"),
    616             Self::MissingLines => write!(f, "trade candidate requires at least one active line"),
    617             Self::TooManyLines { max, actual } => {
    618                 write!(f, "trade candidate has {actual} active lines; max is {max}")
    619             }
    620             Self::TooManyAdjustments { max, actual } => {
    621                 write!(f, "trade economics has {actual} adjustments; max is {max}")
    622             }
    623             Self::DuplicateKey(key) => write!(f, "trade canonical JSON has duplicate key {key}"),
    624             Self::InvalidJson(error) => write!(f, "trade canonical JSON is invalid: {error}"),
    625             Self::NonCanonicalJson => write!(f, "trade JSON is not canonical JCS"),
    626             Self::UnsupportedNumber => write!(f, "trade canonical JSON number is unsupported"),
    627             Self::ContentTooLarge { max, actual } => {
    628                 write!(f, "trade content is {actual} bytes; max is {max}")
    629             }
    630             Self::EmptyField(field) => write!(f, "trade field {field} cannot be empty"),
    631             Self::InvalidField(field) => write!(f, "trade field {field} is invalid"),
    632             Self::InvalidIdentifier { field, error } => {
    633                 write!(f, "trade field {field} is invalid: {error}")
    634             }
    635             Self::InvalidTimeRange => write!(f, "trade fulfillment time range is invalid"),
    636             Self::MissingReservationCommitments => {
    637                 write!(f, "seller reservation assertion requires commitments")
    638             }
    639             Self::MissingCancellationTarget => write!(f, "trade cancellation requires a target"),
    640             Self::CandidateIdMismatch { declared, computed } => write!(
    641                 f,
    642                 "trade candidate id mismatch: declared {declared}, computed {computed}"
    643             ),
    644             Self::MutationIdMismatch { declared, computed } => write!(
    645                 f,
    646                 "trade mutation id mismatch: declared {declared}, computed {computed}"
    647             ),
    648         }
    649     }
    650 }
    651 
    652 #[cfg(feature = "std")]
    653 impl std::error::Error for TradeProtocolError {}
    654 
    655 #[cfg(feature = "serde")]
    656 pub fn canonical_trade_candidate_id(
    657     candidate: &TradeCandidateTermsV1,
    658 ) -> Result<CandidateId, TradeProtocolError> {
    659     let mut value = serialize_trade_value(candidate);
    660     remove_object_field(&mut value, "candidate_id")?;
    661     let canonical = canonical_jcs_value(&value)?;
    662     Ok(trade_candidate_id_from_canonical(canonical.as_bytes()))
    663 }
    664 
    665 #[cfg(feature = "serde")]
    666 pub fn canonical_trade_mutation_id(
    667     envelope: &TradeMutationEnvelopeV1,
    668 ) -> Result<MutationId, TradeProtocolError> {
    669     let mut value = serialize_trade_value(envelope);
    670     remove_object_field(&mut value, "mutation_id")?;
    671     let canonical = canonical_jcs_value(&value)?;
    672     Ok(trade_mutation_id_from_canonical(canonical.as_bytes()))
    673 }
    674 
    675 #[cfg(feature = "serde")]
    676 pub fn canonical_trade_mutation_content(
    677     mut envelope: TradeMutationEnvelopeV1,
    678 ) -> Result<TradeCanonicalMutationV1, TradeProtocolError> {
    679     finalize_body_candidate_ids(&mut envelope.body)?;
    680     envelope.validate()?;
    681     let mutation_id = canonical_trade_mutation_id(&envelope)?;
    682     envelope.mutation_id = Some(mutation_id);
    683     let value = serialize_trade_value(&envelope);
    684     let content = canonical_jcs_value(&value)?;
    685     if content.len() > RADROOTS_TRADE_MAX_PUBLIC_CONTENT_BYTES {
    686         return Err(TradeProtocolError::ContentTooLarge {
    687             max: RADROOTS_TRADE_MAX_PUBLIC_CONTENT_BYTES,
    688             actual: content.len(),
    689         });
    690     }
    691     Ok(TradeCanonicalMutationV1 {
    692         mutation_id,
    693         envelope,
    694         content,
    695     })
    696 }
    697 
    698 #[cfg(feature = "serde")]
    699 pub fn trade_mutation_from_canonical_content(
    700     content: &str,
    701 ) -> Result<TradeMutationEnvelopeV1, TradeProtocolError> {
    702     if content.len() > RADROOTS_TRADE_MAX_PUBLIC_CONTENT_BYTES {
    703         return Err(TradeProtocolError::ContentTooLarge {
    704             max: RADROOTS_TRADE_MAX_PUBLIC_CONTENT_BYTES,
    705             actual: content.len(),
    706         });
    707     }
    708     let value = parse_json_without_duplicate_keys(content)?;
    709     if canonical_jcs_value(&value)? != content {
    710         return Err(TradeProtocolError::NonCanonicalJson);
    711     }
    712     let envelope: TradeMutationEnvelopeV1 = serde_json::from_value(value)
    713         .map_err(|error| TradeProtocolError::InvalidJson(error.to_string()))?;
    714     envelope.validate()?;
    715     verify_candidate_ids(&envelope.body)?;
    716     if let Some(declared) = &envelope.mutation_id {
    717         let computed = canonical_trade_mutation_id(&envelope)?;
    718         if declared != &computed {
    719             return Err(TradeProtocolError::MutationIdMismatch {
    720                 declared: declared.to_string(),
    721                 computed: computed.to_string(),
    722             });
    723         }
    724     }
    725     Ok(envelope)
    726 }
    727 
    728 #[cfg(feature = "serde")]
    729 pub fn canonical_jcs_from_str(content: &str) -> Result<String, TradeProtocolError> {
    730     let value = parse_json_without_duplicate_keys(content)?;
    731     canonical_jcs_value(&value)
    732 }
    733 
    734 #[cfg(feature = "serde")]
    735 pub fn canonical_jcs_value(value: &Value) -> Result<String, TradeProtocolError> {
    736     let mut output = String::new();
    737     write_canonical_jcs(value, &mut output)?;
    738     Ok(output)
    739 }
    740 
    741 #[cfg(feature = "serde")]
    742 fn parse_json_without_duplicate_keys(content: &str) -> Result<Value, TradeProtocolError> {
    743     let mut deserializer = serde_json::Deserializer::from_str(content);
    744     let value = NoDuplicateJsonValue::deserialize(&mut deserializer).map_err(map_json_error)?;
    745     deserializer.end().map_err(map_json_error)?;
    746     Ok(value.0)
    747 }
    748 
    749 #[cfg(feature = "serde")]
    750 fn map_json_error(error: serde_json::Error) -> TradeProtocolError {
    751     let message = error.to_string();
    752     if let Some(key) = message.strip_prefix("duplicate key: ") {
    753         let key = key.split(" at line ").next().unwrap_or(key).to_string();
    754         return TradeProtocolError::DuplicateKey(key);
    755     }
    756     TradeProtocolError::InvalidJson(message)
    757 }
    758 
    759 #[cfg(feature = "serde")]
    760 fn write_canonical_jcs(value: &Value, output: &mut String) -> Result<(), TradeProtocolError> {
    761     match value {
    762         Value::Null => output.push_str("null"),
    763         Value::Bool(value) => output.push_str(if *value { "true" } else { "false" }),
    764         Value::Number(number) => output.push_str(&canonical_number(number)?),
    765         Value::String(value) => {
    766             output.push_str(canonical_json_string(value).as_str());
    767         }
    768         Value::Array(values) => {
    769             output.push('[');
    770             for (index, item) in values.iter().enumerate() {
    771                 if index > 0 {
    772                     output.push(',');
    773                 }
    774                 write_canonical_jcs(item, output)?;
    775             }
    776             output.push(']');
    777         }
    778         Value::Object(map) => {
    779             output.push('{');
    780             let mut keys = map.keys().collect::<Vec<_>>();
    781             keys.sort();
    782             for (index, key) in keys.iter().enumerate() {
    783                 if index > 0 {
    784                     output.push(',');
    785                 }
    786                 output.push_str(canonical_json_string(key.as_str()).as_str());
    787                 output.push(':');
    788                 let value = &map[key.as_str()];
    789                 write_canonical_jcs(value, output)?;
    790             }
    791             output.push('}');
    792         }
    793     }
    794     Ok(())
    795 }
    796 
    797 #[cfg(feature = "serde")]
    798 fn canonical_number(number: &Number) -> Result<String, TradeProtocolError> {
    799     if number.is_i64() || number.is_u64() {
    800         Ok(number.to_string())
    801     } else {
    802         Err(TradeProtocolError::UnsupportedNumber)
    803     }
    804 }
    805 
    806 #[cfg(feature = "serde")]
    807 fn digest_prefixed(domain: &[u8], bytes: &[u8]) -> String {
    808     let mut hasher = Sha256::new();
    809     hasher.update(domain);
    810     hasher.update(bytes);
    811     hex::encode(hasher.finalize())
    812 }
    813 
    814 #[cfg(feature = "serde")]
    815 #[cfg_attr(coverage_nightly, coverage(off))]
    816 fn serialize_trade_value(value: &impl Serialize) -> Value {
    817     serde_json::to_value(value).expect("closed trade models always serialize to JSON values")
    818 }
    819 
    820 #[cfg(feature = "serde")]
    821 #[cfg_attr(coverage_nightly, coverage(off))]
    822 fn canonical_json_string(value: &str) -> String {
    823     serde_json::to_string(value).expect("JSON strings always serialize")
    824 }
    825 
    826 #[cfg(feature = "serde")]
    827 #[cfg_attr(coverage_nightly, coverage(off))]
    828 fn trade_candidate_id_from_canonical(canonical: &[u8]) -> CandidateId {
    829     CandidateId::parse(digest_prefixed(RADROOTS_TRADE_CANDIDATE_DOMAIN, canonical))
    830         .expect("SHA-256 always produces a canonical 64-character identifier")
    831 }
    832 
    833 #[cfg(feature = "serde")]
    834 #[cfg_attr(coverage_nightly, coverage(off))]
    835 fn trade_mutation_id_from_canonical(canonical: &[u8]) -> MutationId {
    836     MutationId::parse(digest_prefixed(RADROOTS_TRADE_MUTATION_DOMAIN, canonical))
    837         .expect("SHA-256 always produces a canonical 64-character identifier")
    838 }
    839 
    840 #[cfg(feature = "serde")]
    841 fn remove_object_field(value: &mut Value, field: &'static str) -> Result<(), TradeProtocolError> {
    842     let Value::Object(map) = value else {
    843         return Err(TradeProtocolError::InvalidJson(
    844             "root must be object".to_string(),
    845         ));
    846     };
    847     map.remove(field);
    848     Ok(())
    849 }
    850 
    851 #[cfg(feature = "serde")]
    852 fn finalize_body_candidate_ids(body: &mut TradeMutationBodyV1) -> Result<(), TradeProtocolError> {
    853     match body {
    854         TradeMutationBodyV1::Proposal { candidate }
    855         | TradeMutationBodyV1::RevisionProposal { candidate } => {
    856             let candidate_id = canonical_trade_candidate_id(candidate)?;
    857             candidate.candidate_id = Some(candidate_id);
    858         }
    859         _ => {}
    860     }
    861     Ok(())
    862 }
    863 
    864 #[cfg(feature = "serde")]
    865 fn verify_candidate_ids(body: &TradeMutationBodyV1) -> Result<(), TradeProtocolError> {
    866     match body {
    867         TradeMutationBodyV1::Proposal { candidate }
    868         | TradeMutationBodyV1::RevisionProposal { candidate } => {
    869             if let Some(declared) = &candidate.candidate_id {
    870                 let computed = canonical_trade_candidate_id(candidate)?;
    871                 if declared != &computed {
    872                     return Err(TradeProtocolError::CandidateIdMismatch {
    873                         declared: declared.to_string(),
    874                         computed: computed.to_string(),
    875                     });
    876                 }
    877             }
    878         }
    879         _ => {}
    880     }
    881     Ok(())
    882 }
    883 
    884 fn validate_parent_mutation_ids(
    885     mutation_id: Option<&MutationId>,
    886     parents: &[MutationId],
    887 ) -> Result<(), TradeProtocolError> {
    888     if parents.len() > RADROOTS_TRADE_MAX_PARENT_MUTATIONS {
    889         return Err(TradeProtocolError::TooManyParents {
    890             max: RADROOTS_TRADE_MAX_PARENT_MUTATIONS,
    891             actual: parents.len(),
    892         });
    893     }
    894     let mut previous: Option<&MutationId> = None;
    895     for parent in parents {
    896         if Some(parent) == mutation_id {
    897             return Err(TradeProtocolError::SelfParent);
    898         }
    899         if let Some(previous) = previous {
    900             match previous.cmp(parent) {
    901                 core::cmp::Ordering::Greater => {
    902                     return Err(TradeProtocolError::UnsortedParents);
    903                 }
    904                 core::cmp::Ordering::Equal => {
    905                     return Err(TradeProtocolError::DuplicateParent);
    906                 }
    907                 core::cmp::Ordering::Less => {}
    908             }
    909         }
    910         previous = Some(parent);
    911     }
    912     Ok(())
    913 }
    914 
    915 fn validate_sorted_unique_by<T, F>(
    916     items: &[T],
    917     mut key: F,
    918     field: &'static str,
    919 ) -> Result<(), TradeProtocolError>
    920 where
    921     F: FnMut(&T) -> &str,
    922 {
    923     let mut previous: Option<&str> = None;
    924     for item in items {
    925         let item_key = key(item);
    926         validate_non_empty(item_key, field)?;
    927         if let Some(previous) = previous
    928             && previous >= item_key
    929         {
    930             return Err(TradeProtocolError::InvalidField(field));
    931         }
    932         previous = Some(item_key);
    933     }
    934     Ok(())
    935 }
    936 
    937 fn validate_non_empty(value: &str, field: &'static str) -> Result<(), TradeProtocolError> {
    938     if value.trim().is_empty() {
    939         Err(TradeProtocolError::EmptyField(field))
    940     } else {
    941         Ok(())
    942     }
    943 }
    944 
    945 fn validate_decimal_string(value: &str, field: &'static str) -> Result<(), TradeProtocolError> {
    946     validate_non_empty(value, field)?;
    947     let mut chars = value.chars();
    948     if matches!(chars.clone().next(), Some('-')) {
    949         chars.next();
    950     }
    951     let mut saw_digit = false;
    952     for character in chars {
    953         match character {
    954             '0'..='9' => saw_digit = true,
    955             _ => return Err(TradeProtocolError::InvalidField(field)),
    956         }
    957     }
    958     if saw_digit {
    959         Ok(())
    960     } else {
    961         Err(TradeProtocolError::InvalidField(field))
    962     }
    963 }
    964 
    965 fn validate_sha256_hex(value: &str, field: &'static str) -> Result<(), TradeProtocolError> {
    966     MutationId::parse(value)
    967         .map(|_| ())
    968         .map_err(|error| TradeProtocolError::InvalidIdentifier { field, error })
    969 }
    970 
    971 #[cfg(feature = "serde")]
    972 struct NoDuplicateJsonValue(Value);
    973 
    974 #[cfg(feature = "serde")]
    975 impl<'de> Deserialize<'de> for NoDuplicateJsonValue {
    976     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    977     where
    978         D: Deserializer<'de>,
    979     {
    980         deserializer.deserialize_any(NoDuplicateJsonValueVisitor)
    981     }
    982 }
    983 
    984 #[cfg(feature = "serde")]
    985 struct NoDuplicateJsonValueVisitor;
    986 
    987 #[cfg(feature = "serde")]
    988 impl<'de> Visitor<'de> for NoDuplicateJsonValueVisitor {
    989     type Value = NoDuplicateJsonValue;
    990 
    991     fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    992         formatter.write_str("JSON value without duplicate object keys")
    993     }
    994 
    995     fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E> {
    996         Ok(NoDuplicateJsonValue(Value::Bool(value)))
    997     }
    998 
    999     fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E> {
   1000         Ok(NoDuplicateJsonValue(Value::Number(value.into())))
   1001     }
   1002 
   1003     fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E> {
   1004         Ok(NoDuplicateJsonValue(Value::Number(value.into())))
   1005     }
   1006 
   1007     fn visit_f64<E>(self, _value: f64) -> Result<Self::Value, E>
   1008     where
   1009         E: serde::de::Error,
   1010     {
   1011         Err(E::custom("unsupported number"))
   1012     }
   1013 
   1014     fn visit_str<E>(self, value: &str) -> Result<Self::Value, E> {
   1015         Ok(NoDuplicateJsonValue(Value::String(value.to_string())))
   1016     }
   1017 
   1018     fn visit_string<E>(self, value: String) -> Result<Self::Value, E> {
   1019         Ok(NoDuplicateJsonValue(Value::String(value)))
   1020     }
   1021 
   1022     fn visit_none<E>(self) -> Result<Self::Value, E> {
   1023         Ok(NoDuplicateJsonValue(Value::Null))
   1024     }
   1025 
   1026     fn visit_unit<E>(self) -> Result<Self::Value, E> {
   1027         Ok(NoDuplicateJsonValue(Value::Null))
   1028     }
   1029 
   1030     fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
   1031     where
   1032         D: Deserializer<'de>,
   1033     {
   1034         Deserialize::deserialize(deserializer)
   1035     }
   1036 
   1037     fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
   1038     where
   1039         A: SeqAccess<'de>,
   1040     {
   1041         let mut values = Vec::new();
   1042         while let Some(value) = sequence.next_element::<NoDuplicateJsonValue>()? {
   1043             values.push(value.0);
   1044         }
   1045         Ok(NoDuplicateJsonValue(Value::Array(values)))
   1046     }
   1047 
   1048     fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
   1049     where
   1050         A: MapAccess<'de>,
   1051     {
   1052         let mut values = BTreeMap::new();
   1053         while let Some(key) = map.next_key::<String>()? {
   1054             if values.contains_key(&key) {
   1055                 return Err(A::Error::custom(format!("duplicate key: {key}")));
   1056             }
   1057             let value = map.next_value::<NoDuplicateJsonValue>()?;
   1058             values.insert(key, value.0);
   1059         }
   1060         let mut object = Map::new();
   1061         for (key, value) in values {
   1062             object.insert(key, value);
   1063         }
   1064         Ok(NoDuplicateJsonValue(Value::Object(object)))
   1065     }
   1066 }
   1067 
   1068 #[cfg(all(test, feature = "serde"))]
   1069 #[cfg_attr(coverage_nightly, coverage(off))]
   1070 mod tests {
   1071     use super::*;
   1072     use crate::id::parse_public_key;
   1073 
   1074     fn hex_64(character: char) -> String {
   1075         core::iter::repeat_n(character, 64).collect()
   1076     }
   1077 
   1078     fn hex_32(character: char) -> String {
   1079         core::iter::repeat_n(character, 32).collect()
   1080     }
   1081 
   1082     fn pubkey(character: char) -> PublicKey {
   1083         parse_public_key(crate::test_valid_hex_64(character)).unwrap()
   1084     }
   1085 
   1086     fn event_id(character: char) -> EventId {
   1087         EventId::parse(hex_64(character)).unwrap()
   1088     }
   1089 
   1090     fn trade_id() -> TradeId {
   1091         TradeId::parse(hex_32('1')).unwrap()
   1092     }
   1093 
   1094     fn mutation_id(character: char) -> MutationId {
   1095         MutationId::parse(hex_64(character)).unwrap()
   1096     }
   1097 
   1098     fn candidate_id(character: char) -> CandidateId {
   1099         CandidateId::parse(hex_64(character)).unwrap()
   1100     }
   1101 
   1102     fn candidate() -> TradeCandidateTermsV1 {
   1103         TradeCandidateTermsV1 {
   1104             candidate_id: None,
   1105             schema_version: RADROOTS_TRADE_SCHEMA_VERSION,
   1106             base_candidate_id: None,
   1107             supersession_intent: None,
   1108             buyer_pubkey: pubkey('a'),
   1109             seller_pubkey: pubkey('b'),
   1110             farm_id: DTag::parse("farm-1").unwrap(),
   1111             lines: vec![TradeCandidateLineV1 {
   1112                 line_id: DTag::parse("line-1").unwrap(),
   1113                 listing_addr: ClassifiedListingAddress::parse(format!(
   1114                     "30402:{}:listing-1",
   1115                     crate::test_valid_hex_64('b')
   1116                 ))
   1117                 .unwrap(),
   1118                 listing_event_id: event_id('c'),
   1119                 listing_snapshot_sha256: hex_64('d'),
   1120                 product_id: "carrots".to_string(),
   1121                 option_id: None,
   1122                 bin_id: InventoryBinId::parse("bin-1").unwrap(),
   1123                 quantity_mantissa: "2".to_string(),
   1124                 quantity_scale: 0,
   1125                 unit_code: "count".to_string(),
   1126                 unit_profile: "mvp-count".to_string(),
   1127                 unit_price_mantissa: "500".to_string(),
   1128                 currency_code: "USD".to_string(),
   1129                 line_subtotal_mantissa: "1000".to_string(),
   1130                 replaces_line_id: None,
   1131             }],
   1132             line_tombstones: Vec::new(),
   1133             economics: TradeEconomicsProfileV1 {
   1134                 profile_id: "mvp-fixed".to_string(),
   1135                 currency_code: "USD".to_string(),
   1136                 currency_exponent: 2,
   1137                 rounding_profile: "half-even".to_string(),
   1138                 subtotal_mantissa: "1000".to_string(),
   1139                 discount_total_mantissa: "0".to_string(),
   1140                 adjustment_total_mantissa: "0".to_string(),
   1141                 total_mantissa: "1000".to_string(),
   1142                 adjustments: Vec::new(),
   1143             },
   1144             fulfillment: FulfillmentProfileV1 {
   1145                 profile_id: "market-pickup".to_string(),
   1146                 method: "pickup".to_string(),
   1147                 starts_at_unix_s: 1_800_000_000,
   1148                 ends_at_unix_s: 1_800_003_600,
   1149                 timezone: "America/New_York".to_string(),
   1150                 utc_offset_seconds: -18_000,
   1151                 fold: 0,
   1152                 location_class: "farmstand".to_string(),
   1153                 requires_private_terms: false,
   1154             },
   1155             cancellation: TradeCancellationProfileV1 {
   1156                 profile_id: "buyer-pre-agreement".to_string(),
   1157                 buyer_pre_agreement: true,
   1158                 post_agreement_cutoff_unix_s: None,
   1159             },
   1160             private_terms: None,
   1161             proposal_expires_at_unix_s: 1_799_999_000,
   1162         }
   1163     }
   1164 
   1165     fn proposal() -> TradeMutationEnvelopeV1 {
   1166         TradeMutationEnvelopeV1 {
   1167             mutation_id: None,
   1168             contract_id: RADROOTS_TRADE_PROPOSAL_CONTRACT_ID.to_string(),
   1169             schema_version: RADROOTS_TRADE_SCHEMA_VERSION,
   1170             trade_id: trade_id(),
   1171             root_mutation_id: None,
   1172             buyer_pubkey: pubkey('a'),
   1173             seller_pubkey: pubkey('b'),
   1174             farm_id: DTag::parse("farm-1").unwrap(),
   1175             parent_mutation_ids: Vec::new(),
   1176             author_pubkey: pubkey('a'),
   1177             counterparty_pubkey: pubkey('b'),
   1178             authored_at_unix_s: 1_799_000_000,
   1179             body: TradeMutationBodyV1::Proposal {
   1180                 candidate: candidate(),
   1181             },
   1182         }
   1183     }
   1184 
   1185     fn adjustment(id: &str) -> TradeEconomicAdjustmentV1 {
   1186         TradeEconomicAdjustmentV1 {
   1187             adjustment_id: DTag::parse(id).unwrap(),
   1188             actor: "seller".to_owned(),
   1189             effect: "charge".to_owned(),
   1190             amount_mantissa: "10".to_owned(),
   1191             reason: "packing".to_owned(),
   1192         }
   1193     }
   1194 
   1195     fn reservation_assertion() -> SellerReservationAssertionV1 {
   1196         SellerReservationAssertionV1 {
   1197             reservation_id: DTag::parse("reservation-1").unwrap(),
   1198             inventory_authority_id: pubkey('c'),
   1199             inventory_epoch: 1,
   1200             candidate_id: candidate_id('d'),
   1201             commitments: vec![SellerReservationLineV1 {
   1202                 line_id: DTag::parse("line-1").unwrap(),
   1203                 bin_id: InventoryBinId::parse("bin-1").unwrap(),
   1204                 quantity_mantissa: "2".to_owned(),
   1205                 quantity_scale: 0,
   1206                 unit_code: "count".to_owned(),
   1207             }],
   1208             reservation_expires_at_unix_s: 1_800_000_000,
   1209             assertion_commitment: hex_64('e'),
   1210         }
   1211     }
   1212 
   1213     fn child_envelope(body: TradeMutationBodyV1) -> TradeMutationEnvelopeV1 {
   1214         let mut envelope = proposal();
   1215         envelope.contract_id = body.mutation_kind().contract_id().to_owned();
   1216         envelope.root_mutation_id = Some(mutation_id('a'));
   1217         envelope.parent_mutation_ids = vec![mutation_id('a')];
   1218         envelope.body = body;
   1219         envelope
   1220     }
   1221 
   1222     #[test]
   1223     fn canonical_json_sorts_keys_and_rejects_duplicate_keys() {
   1224         assert_eq!(
   1225             canonical_jcs_from_str(r#"{"z":1,"a":["b",true,null]}"#).unwrap(),
   1226             r#"{"a":["b",true,null],"z":1}"#
   1227         );
   1228         assert!(matches!(
   1229             canonical_jcs_from_str(r#"{"a":1,"a":2}"#),
   1230             Err(TradeProtocolError::DuplicateKey(_))
   1231         ));
   1232     }
   1233 
   1234     #[test]
   1235     fn canonical_trade_mutation_sets_candidate_and_mutation_ids() {
   1236         let canonical = canonical_trade_mutation_content(proposal()).unwrap();
   1237         assert!(canonical.envelope.mutation_id.is_some());
   1238         assert_eq!(
   1239             canonical.mutation_id,
   1240             canonical.envelope.mutation_id.unwrap()
   1241         );
   1242         let parsed = trade_mutation_from_canonical_content(&canonical.content).unwrap();
   1243         assert_eq!(parsed.mutation_id, Some(canonical.mutation_id));
   1244     }
   1245 
   1246     #[test]
   1247     fn canonical_trade_mutation_rejects_parent_ordering() {
   1248         let mut envelope = proposal();
   1249         envelope.contract_id = RADROOTS_TRADE_REVISION_PROPOSAL_CONTRACT_ID.to_string();
   1250         envelope.root_mutation_id = Some(MutationId::parse(hex_64('a')).unwrap());
   1251         envelope.parent_mutation_ids = vec![
   1252             MutationId::parse(hex_64('b')).unwrap(),
   1253             MutationId::parse(hex_64('a')).unwrap(),
   1254         ];
   1255         envelope.body = TradeMutationBodyV1::RevisionProposal {
   1256             candidate: candidate(),
   1257         };
   1258         assert!(matches!(
   1259             canonical_trade_mutation_content(envelope),
   1260             Err(TradeProtocolError::UnsortedParents)
   1261         ));
   1262     }
   1263 
   1264     #[test]
   1265     #[cfg_attr(coverage_nightly, coverage(off))]
   1266     fn trade_validation_contract_covers_every_mutation_and_parent_rule() {
   1267         let kinds = [
   1268             TradeMutationKindV1::Proposal,
   1269             TradeMutationKindV1::Decision,
   1270             TradeMutationKindV1::RevisionProposal,
   1271             TradeMutationKindV1::RevisionDecision,
   1272             TradeMutationKindV1::Cancellation,
   1273         ];
   1274         for kind in kinds {
   1275             assert!(!kind.contract_id().is_empty());
   1276             assert_ne!(kind.nostr_kind(), 0);
   1277         }
   1278 
   1279         let mut envelope = proposal();
   1280         envelope.schema_version += 1;
   1281         assert!(matches!(
   1282             envelope.validate(),
   1283             Err(TradeProtocolError::InvalidSchemaVersion { .. })
   1284         ));
   1285         let mut envelope = proposal();
   1286         envelope.contract_id = "wrong".to_owned();
   1287         assert!(matches!(
   1288             envelope.validate(),
   1289             Err(TradeProtocolError::ContractMismatch { .. })
   1290         ));
   1291         let mut envelope = proposal();
   1292         envelope.root_mutation_id = Some(mutation_id('a'));
   1293         assert_eq!(
   1294             envelope.validate(),
   1295             Err(TradeProtocolError::InvalidInitialParents)
   1296         );
   1297         let mut envelope = proposal();
   1298         envelope.parent_mutation_ids = vec![mutation_id('a')];
   1299         assert_eq!(
   1300             envelope.validate(),
   1301             Err(TradeProtocolError::InvalidInitialParents)
   1302         );
   1303 
   1304         let mut envelope = child_envelope(TradeMutationBodyV1::Decision {
   1305             proposal_mutation_id: mutation_id('a'),
   1306             candidate_id: candidate_id('a'),
   1307             decision: TradeDecisionV1::Accepted {
   1308                 reservation_assertion: None,
   1309             },
   1310         });
   1311         assert!(envelope.validate().is_ok());
   1312         envelope.root_mutation_id = None;
   1313         assert_eq!(
   1314             envelope.validate(),
   1315             Err(TradeProtocolError::MissingParentMutation)
   1316         );
   1317         let mut envelope = child_envelope(TradeMutationBodyV1::Decision {
   1318             proposal_mutation_id: mutation_id('a'),
   1319             candidate_id: candidate_id('a'),
   1320             decision: TradeDecisionV1::Accepted {
   1321                 reservation_assertion: None,
   1322             },
   1323         });
   1324         envelope.parent_mutation_ids.clear();
   1325         assert_eq!(
   1326             envelope.validate(),
   1327             Err(TradeProtocolError::MissingParentMutation)
   1328         );
   1329 
   1330         let revision_decision = child_envelope(TradeMutationBodyV1::RevisionDecision {
   1331             proposal_mutation_id: mutation_id('a'),
   1332             candidate_id: candidate_id('a'),
   1333             decision: TradeDecisionV1::Declined {
   1334                 reason: "inventory unavailable".to_owned(),
   1335             },
   1336         });
   1337         assert!(revision_decision.validate().is_ok());
   1338         let revision = child_envelope(TradeMutationBodyV1::RevisionProposal {
   1339             candidate: candidate(),
   1340         });
   1341         assert!(revision.validate().is_ok());
   1342 
   1343         for body in [
   1344             TradeMutationBodyV1::Cancellation {
   1345                 target_candidate_id: Some(candidate_id('a')),
   1346                 target_claim_mutation_id: None,
   1347                 reason: "cancelled".to_owned(),
   1348             },
   1349             TradeMutationBodyV1::Cancellation {
   1350                 target_candidate_id: None,
   1351                 target_claim_mutation_id: Some(mutation_id('a')),
   1352                 reason: "cancelled".to_owned(),
   1353             },
   1354         ] {
   1355             assert!(child_envelope(body).validate().is_ok());
   1356         }
   1357         assert_eq!(
   1358             TradeMutationBodyV1::Cancellation {
   1359                 target_candidate_id: None,
   1360                 target_claim_mutation_id: None,
   1361                 reason: "cancelled".to_owned(),
   1362             }
   1363             .validate(),
   1364             Err(TradeProtocolError::MissingCancellationTarget)
   1365         );
   1366         assert!(
   1367             TradeMutationBodyV1::Cancellation {
   1368                 target_candidate_id: Some(candidate_id('a')),
   1369                 target_claim_mutation_id: None,
   1370                 reason: " ".to_owned(),
   1371             }
   1372             .validate()
   1373             .is_err()
   1374         );
   1375 
   1376         assert_eq!(
   1377             validate_parent_mutation_ids(
   1378                 None,
   1379                 &[
   1380                     mutation_id('a'),
   1381                     mutation_id('b'),
   1382                     mutation_id('c'),
   1383                     mutation_id('d'),
   1384                     mutation_id('e'),
   1385                 ],
   1386             ),
   1387             Err(TradeProtocolError::TooManyParents {
   1388                 max: RADROOTS_TRADE_MAX_PARENT_MUTATIONS,
   1389                 actual: 5,
   1390             })
   1391         );
   1392         assert_eq!(
   1393             validate_parent_mutation_ids(None, &[mutation_id('a'), mutation_id('a')]),
   1394             Err(TradeProtocolError::DuplicateParent)
   1395         );
   1396         assert_eq!(
   1397             validate_parent_mutation_ids(Some(&mutation_id('a')), &[mutation_id('a')]),
   1398             Err(TradeProtocolError::SelfParent)
   1399         );
   1400         assert!(validate_parent_mutation_ids(None, &[mutation_id('a'), mutation_id('b')]).is_ok());
   1401         assert!(validate_sorted_unique_by(&["a", "b"], |value| *value, "values").is_ok());
   1402     }
   1403 
   1404     #[test]
   1405     #[cfg_attr(coverage_nightly, coverage(off))]
   1406     fn trade_candidate_validation_covers_every_nested_profile() {
   1407         let base = candidate();
   1408         assert!(base.validate().is_ok());
   1409 
   1410         let mut invalid = base.clone();
   1411         invalid.schema_version += 1;
   1412         assert!(invalid.validate().is_err());
   1413         let mut invalid = base.clone();
   1414         invalid.lines.clear();
   1415         assert_eq!(invalid.validate(), Err(TradeProtocolError::MissingLines));
   1416         let mut invalid = base.clone();
   1417         invalid.lines = vec![base.lines[0].clone(); RADROOTS_TRADE_MAX_ACTIVE_LINES + 1];
   1418         assert!(matches!(
   1419             invalid.validate(),
   1420             Err(TradeProtocolError::TooManyLines { .. })
   1421         ));
   1422         let mut invalid = base.clone();
   1423         invalid.lines.push(base.lines[0].clone());
   1424         assert!(matches!(
   1425             invalid.validate(),
   1426             Err(TradeProtocolError::InvalidField("lines"))
   1427         ));
   1428 
   1429         let tombstone = TradeLineTombstoneV1 {
   1430             line_id: DTag::parse("line-2").unwrap(),
   1431             reason: "removed".to_owned(),
   1432         };
   1433         let mut with_tombstone = base.clone();
   1434         with_tombstone.line_tombstones.push(tombstone.clone());
   1435         assert!(with_tombstone.validate().is_ok());
   1436         let mut invalid = with_tombstone.clone();
   1437         invalid.line_tombstones.push(tombstone);
   1438         assert!(invalid.validate().is_err());
   1439         let mut invalid = with_tombstone;
   1440         invalid.line_tombstones[0].reason = " ".to_owned();
   1441         assert!(invalid.validate().is_err());
   1442 
   1443         let mut line = base.lines[0].clone();
   1444         line.option_id = Some("option-1".to_owned());
   1445         assert!(line.validate().is_ok());
   1446         for mutate in [
   1447             |line: &mut TradeCandidateLineV1| line.listing_snapshot_sha256 = "bad".to_owned(),
   1448             |line: &mut TradeCandidateLineV1| line.product_id = " ".to_owned(),
   1449             |line: &mut TradeCandidateLineV1| line.option_id = Some(" ".to_owned()),
   1450             |line: &mut TradeCandidateLineV1| line.quantity_mantissa = "1.5".to_owned(),
   1451             |line: &mut TradeCandidateLineV1| line.unit_code = " ".to_owned(),
   1452             |line: &mut TradeCandidateLineV1| line.unit_profile = " ".to_owned(),
   1453             |line: &mut TradeCandidateLineV1| line.unit_price_mantissa = "-".to_owned(),
   1454             |line: &mut TradeCandidateLineV1| line.currency_code = " ".to_owned(),
   1455             |line: &mut TradeCandidateLineV1| line.line_subtotal_mantissa = "x".to_owned(),
   1456         ] {
   1457             let mut line = base.lines[0].clone();
   1458             mutate(&mut line);
   1459             assert!(line.validate().is_err());
   1460         }
   1461         let mut line = base.lines[0].clone();
   1462         line.quantity_mantissa = "-2".to_owned();
   1463         assert!(line.validate().is_ok());
   1464 
   1465         let economics = base.economics.clone();
   1466         for mutate in [
   1467             |value: &mut TradeEconomicsProfileV1| value.profile_id = " ".to_owned(),
   1468             |value: &mut TradeEconomicsProfileV1| value.currency_code = " ".to_owned(),
   1469             |value: &mut TradeEconomicsProfileV1| value.rounding_profile = " ".to_owned(),
   1470             |value: &mut TradeEconomicsProfileV1| value.subtotal_mantissa = "x".to_owned(),
   1471             |value: &mut TradeEconomicsProfileV1| value.discount_total_mantissa = "x".to_owned(),
   1472             |value: &mut TradeEconomicsProfileV1| value.adjustment_total_mantissa = "x".to_owned(),
   1473             |value: &mut TradeEconomicsProfileV1| value.total_mantissa = "x".to_owned(),
   1474         ] {
   1475             let mut value = economics.clone();
   1476             mutate(&mut value);
   1477             assert!(value.validate().is_err());
   1478         }
   1479         let mut value = economics.clone();
   1480         value.adjustments = vec![adjustment("adjustment-1"); RADROOTS_TRADE_MAX_ADJUSTMENTS + 1];
   1481         assert!(matches!(
   1482             value.validate(),
   1483             Err(TradeProtocolError::TooManyAdjustments { .. })
   1484         ));
   1485         let mut value = economics.clone();
   1486         value.adjustments = vec![adjustment("adjustment-1"), adjustment("adjustment-1")];
   1487         assert!(value.validate().is_err());
   1488         let mut value = economics.clone();
   1489         value.adjustments = vec![adjustment("adjustment-1")];
   1490         assert!(value.validate().is_ok());
   1491         for mutate in [
   1492             |value: &mut TradeEconomicAdjustmentV1| value.actor = " ".to_owned(),
   1493             |value: &mut TradeEconomicAdjustmentV1| value.effect = " ".to_owned(),
   1494             |value: &mut TradeEconomicAdjustmentV1| value.amount_mantissa = "x".to_owned(),
   1495             |value: &mut TradeEconomicAdjustmentV1| value.reason = " ".to_owned(),
   1496         ] {
   1497             let mut value = adjustment("adjustment-1");
   1498             mutate(&mut value);
   1499             assert!(value.validate().is_err());
   1500         }
   1501 
   1502         let fulfillment = base.fulfillment.clone();
   1503         for mutate in [
   1504             |value: &mut FulfillmentProfileV1| value.profile_id = " ".to_owned(),
   1505             |value: &mut FulfillmentProfileV1| value.method = " ".to_owned(),
   1506             |value: &mut FulfillmentProfileV1| value.timezone = " ".to_owned(),
   1507             |value: &mut FulfillmentProfileV1| value.location_class = " ".to_owned(),
   1508             |value: &mut FulfillmentProfileV1| value.ends_at_unix_s = value.starts_at_unix_s,
   1509             |value: &mut FulfillmentProfileV1| value.fold = 2,
   1510         ] {
   1511             let mut value = fulfillment.clone();
   1512             mutate(&mut value);
   1513             assert!(value.validate().is_err());
   1514         }
   1515         let mut cancellation = base.cancellation.clone();
   1516         cancellation.profile_id = " ".to_owned();
   1517         assert!(cancellation.validate().is_err());
   1518 
   1519         let private_terms = TradePrivateTermsRefV1 {
   1520             artifact_id: "artifact-1".to_owned(),
   1521             schema_id: "schema-1".to_owned(),
   1522             ciphertext_commitment: hex_64('a'),
   1523             required_acknowledgement: true,
   1524         };
   1525         let mut with_private = base.clone();
   1526         with_private.private_terms = Some(private_terms.clone());
   1527         assert!(with_private.validate().is_ok());
   1528         for mutate in [
   1529             |value: &mut TradePrivateTermsRefV1| value.artifact_id = " ".to_owned(),
   1530             |value: &mut TradePrivateTermsRefV1| value.schema_id = " ".to_owned(),
   1531             |value: &mut TradePrivateTermsRefV1| value.ciphertext_commitment = "bad".to_owned(),
   1532         ] {
   1533             let mut value = private_terms.clone();
   1534             mutate(&mut value);
   1535             assert!(value.validate().is_err());
   1536         }
   1537     }
   1538 
   1539     #[test]
   1540     fn trade_candidate_deserialization_rejects_non_classified_listing_coordinates() {
   1541         let mut value = serde_json::to_value(candidate()).expect("candidate json");
   1542         value["lines"][0]["listing_addr"] =
   1543             serde_json::json!(format!("30023:{}:listing-1", crate::test_valid_hex_64('b')));
   1544 
   1545         assert!(serde_json::from_value::<TradeCandidateTermsV1>(value).is_err());
   1546     }
   1547 
   1548     #[test]
   1549     #[cfg_attr(coverage_nightly, coverage(off))]
   1550     fn trade_decision_and_reservation_contracts_cover_all_paths() {
   1551         let assertion = reservation_assertion();
   1552         assert!(assertion.validate().is_ok());
   1553         assert!(
   1554             TradeDecisionV1::Accepted {
   1555                 reservation_assertion: None,
   1556             }
   1557             .validate()
   1558             .is_ok()
   1559         );
   1560         assert!(
   1561             TradeDecisionV1::Accepted {
   1562                 reservation_assertion: Some(assertion.clone()),
   1563             }
   1564             .validate()
   1565             .is_ok()
   1566         );
   1567         assert!(
   1568             TradeDecisionV1::Declined {
   1569                 reason: "declined".to_owned(),
   1570             }
   1571             .validate()
   1572             .is_ok()
   1573         );
   1574         assert!(
   1575             TradeDecisionV1::Declined {
   1576                 reason: " ".to_owned(),
   1577             }
   1578             .validate()
   1579             .is_err()
   1580         );
   1581 
   1582         let mut invalid = assertion.clone();
   1583         invalid.commitments.clear();
   1584         assert_eq!(
   1585             invalid.validate(),
   1586             Err(TradeProtocolError::MissingReservationCommitments)
   1587         );
   1588         let mut invalid = assertion.clone();
   1589         invalid.commitments.push(invalid.commitments[0].clone());
   1590         assert!(invalid.validate().is_err());
   1591         let mut invalid = assertion.clone();
   1592         invalid.commitments[0].quantity_mantissa = "x".to_owned();
   1593         assert!(invalid.validate().is_err());
   1594         let mut invalid = assertion.clone();
   1595         invalid.commitments[0].unit_code = " ".to_owned();
   1596         assert!(invalid.validate().is_err());
   1597         let mut invalid = assertion;
   1598         invalid.assertion_commitment = "bad".to_owned();
   1599         assert!(invalid.validate().is_err());
   1600     }
   1601 
   1602     #[test]
   1603     #[cfg_attr(coverage_nightly, coverage(off))]
   1604     fn trade_canonicalization_and_error_contracts_cover_all_paths() {
   1605         use serde::de::{Unexpected, value::StrDeserializer};
   1606 
   1607         let canonical = canonical_trade_mutation_content(proposal()).expect("canonical proposal");
   1608         assert_eq!(
   1609             canonical_trade_candidate_id(match &canonical.envelope.body {
   1610                 TradeMutationBodyV1::Proposal { candidate } => candidate,
   1611                 _ => unreachable!("proposal"),
   1612             })
   1613             .expect("candidate id"),
   1614             match &canonical.envelope.body {
   1615                 TradeMutationBodyV1::Proposal { candidate } => {
   1616                     candidate.candidate_id.expect("candidate id")
   1617                 }
   1618                 _ => unreachable!("proposal"),
   1619             }
   1620         );
   1621         assert_eq!(
   1622             canonical_trade_mutation_id(&canonical.envelope).expect("mutation id"),
   1623             canonical.mutation_id
   1624         );
   1625 
   1626         assert!(matches!(
   1627             trade_mutation_from_canonical_content(
   1628                 &"x".repeat(RADROOTS_TRADE_MAX_PUBLIC_CONTENT_BYTES + 1)
   1629             ),
   1630             Err(TradeProtocolError::ContentTooLarge { .. })
   1631         ));
   1632         assert_eq!(
   1633             trade_mutation_from_canonical_content(" {} "),
   1634             Err(TradeProtocolError::NonCanonicalJson)
   1635         );
   1636         assert!(matches!(
   1637             trade_mutation_from_canonical_content("{}"),
   1638             Err(TradeProtocolError::InvalidJson(_))
   1639         ));
   1640         assert!(matches!(
   1641             canonical_jcs_from_str("{"),
   1642             Err(TradeProtocolError::InvalidJson(_))
   1643         ));
   1644         assert!(matches!(
   1645             canonical_jcs_from_str("1.5"),
   1646             Err(TradeProtocolError::InvalidJson(_))
   1647         ));
   1648         assert_eq!(
   1649             canonical_jcs_value(&serde_json::json!(1.5)),
   1650             Err(TradeProtocolError::UnsupportedNumber)
   1651         );
   1652         assert_eq!(
   1653             canonical_jcs_value(&Value::Number(Number::from(u64::MAX))).expect("large integer"),
   1654             u64::MAX.to_string()
   1655         );
   1656         assert!(remove_object_field(&mut Value::Null, "id").is_err());
   1657 
   1658         let mut value: Value = serde_json::from_str(&canonical.content).expect("canonical json");
   1659         value["body"]["candidate"]["candidate_id"] = Value::String(hex_64('f'));
   1660         let wrong_candidate = canonical_jcs_value(&value).expect("wrong candidate json");
   1661         assert!(matches!(
   1662             trade_mutation_from_canonical_content(&wrong_candidate),
   1663             Err(TradeProtocolError::CandidateIdMismatch { .. })
   1664         ));
   1665 
   1666         let mut value: Value = serde_json::from_str(&canonical.content).expect("canonical json");
   1667         value["mutation_id"] = Value::String(hex_64('f'));
   1668         let wrong_mutation = canonical_jcs_value(&value).expect("wrong mutation json");
   1669         assert!(matches!(
   1670             trade_mutation_from_canonical_content(&wrong_mutation),
   1671             Err(TradeProtocolError::MutationIdMismatch { .. })
   1672         ));
   1673 
   1674         let mut value: Value = serde_json::from_str(&canonical.content).expect("canonical json");
   1675         value["mutation_id"] = Value::Null;
   1676         value["body"]["candidate"]["candidate_id"] = Value::Null;
   1677         let undeclared_ids = canonical_jcs_value(&value).expect("undeclared ids json");
   1678         assert!(trade_mutation_from_canonical_content(&undeclared_ids).is_ok());
   1679 
   1680         let decision = child_envelope(TradeMutationBodyV1::Decision {
   1681             proposal_mutation_id: mutation_id('a'),
   1682             candidate_id: candidate_id('a'),
   1683             decision: TradeDecisionV1::Accepted {
   1684                 reservation_assertion: None,
   1685             },
   1686         });
   1687         let decision = canonical_trade_mutation_content(decision).expect("canonical decision");
   1688         assert!(trade_mutation_from_canonical_content(&decision.content).is_ok());
   1689 
   1690         let revision = child_envelope(TradeMutationBodyV1::RevisionProposal {
   1691             candidate: candidate(),
   1692         });
   1693         let revision = canonical_trade_mutation_content(revision).expect("canonical revision");
   1694         assert!(trade_mutation_from_canonical_content(&revision.content).is_ok());
   1695 
   1696         let mut oversized = proposal();
   1697         if let TradeMutationBodyV1::Proposal { candidate } = &mut oversized.body {
   1698             candidate.lines[0].product_id = "x".repeat(RADROOTS_TRADE_MAX_PUBLIC_CONTENT_BYTES + 1);
   1699         }
   1700         assert!(matches!(
   1701             canonical_trade_mutation_content(oversized),
   1702             Err(TradeProtocolError::ContentTooLarge { .. })
   1703         ));
   1704 
   1705         let string_value: Result<NoDuplicateJsonValue, serde_json::Error> =
   1706             NoDuplicateJsonValueVisitor.visit_string("value".to_owned());
   1707         assert_eq!(
   1708             string_value.expect("string").0,
   1709             Value::String("value".to_owned())
   1710         );
   1711         let none_value: Result<NoDuplicateJsonValue, serde_json::Error> =
   1712             NoDuplicateJsonValueVisitor.visit_none();
   1713         assert_eq!(none_value.expect("none").0, Value::Null);
   1714         let some_value: Result<NoDuplicateJsonValue, serde_json::Error> =
   1715             NoDuplicateJsonValueVisitor.visit_some(StrDeserializer::new("value"));
   1716         assert_eq!(
   1717             some_value.expect("some").0,
   1718             Value::String("value".to_owned())
   1719         );
   1720         let expected = <serde_json::Error as serde::de::Error>::invalid_type(
   1721             Unexpected::Bool(true),
   1722             &NoDuplicateJsonValueVisitor,
   1723         );
   1724         assert!(expected.to_string().contains("JSON value"));
   1725 
   1726         let parse_error = MutationId::parse("bad").expect_err("invalid id");
   1727         let errors = [
   1728             TradeProtocolError::InvalidSchemaVersion {
   1729                 expected: 1,
   1730                 actual: 2,
   1731             },
   1732             TradeProtocolError::ContractMismatch {
   1733                 expected: "expected",
   1734                 actual: "actual".to_owned(),
   1735             },
   1736             TradeProtocolError::InvalidInitialParents,
   1737             TradeProtocolError::MissingParentMutation,
   1738             TradeProtocolError::TooManyParents { max: 1, actual: 2 },
   1739             TradeProtocolError::UnsortedParents,
   1740             TradeProtocolError::DuplicateParent,
   1741             TradeProtocolError::SelfParent,
   1742             TradeProtocolError::MissingLines,
   1743             TradeProtocolError::TooManyLines { max: 1, actual: 2 },
   1744             TradeProtocolError::TooManyAdjustments { max: 1, actual: 2 },
   1745             TradeProtocolError::DuplicateKey("key".to_owned()),
   1746             TradeProtocolError::InvalidJson("json".to_owned()),
   1747             TradeProtocolError::NonCanonicalJson,
   1748             TradeProtocolError::UnsupportedNumber,
   1749             TradeProtocolError::ContentTooLarge { max: 1, actual: 2 },
   1750             TradeProtocolError::EmptyField("field"),
   1751             TradeProtocolError::InvalidField("field"),
   1752             TradeProtocolError::InvalidIdentifier {
   1753                 field: "field",
   1754                 error: parse_error,
   1755             },
   1756             TradeProtocolError::InvalidTimeRange,
   1757             TradeProtocolError::MissingReservationCommitments,
   1758             TradeProtocolError::MissingCancellationTarget,
   1759             TradeProtocolError::CandidateIdMismatch {
   1760                 declared: "a".to_owned(),
   1761                 computed: "b".to_owned(),
   1762             },
   1763             TradeProtocolError::MutationIdMismatch {
   1764                 declared: "a".to_owned(),
   1765                 computed: "b".to_owned(),
   1766             },
   1767         ];
   1768         for error in errors {
   1769             assert!(!error.to_string().is_empty());
   1770         }
   1771     }
   1772 }