lib

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

lib.rs (26746B)


      1 //! Private mobile FFI over the shared SDK engine.
      2 
      3 #![forbid(unsafe_code)]
      4 #![doc = include_str!("../README.md")]
      5 
      6 use std::sync::Arc;
      7 
      8 use radroots_sdk::{Client, ClientBuilder};
      9 
     10 uniffi::setup_scaffolding!("radroots_sdk");
     11 
     12 /// Generation-1 mobile DTOs.
     13 pub mod v1 {
     14     use super::*;
     15 
     16     /// Stable capability maturity independent of runtime availability.
     17     #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
     18     pub enum CapabilityMaturity {
     19         Stable,
     20         Preview,
     21         Experimental,
     22     }
     23 
     24     /// Current side-effect-free capability availability.
     25     #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
     26     pub enum CapabilityAvailability {
     27         Available,
     28         Degraded,
     29         Unavailable,
     30         Unsupported,
     31     }
     32 
     33     /// Versioned, presentation-independent capability observation.
     34     #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
     35     pub struct CapabilityStatus {
     36         pub id: String,
     37         pub compiled: bool,
     38         pub configured: bool,
     39         pub availability: CapabilityAvailability,
     40         pub maturity: CapabilityMaturity,
     41     }
     42 
     43     /// Secret-safe SDK failure at the language boundary.
     44     #[derive(Debug, thiserror::Error, uniffi::Error)]
     45     pub enum Error {
     46         #[error("SDK {code}: {message}")]
     47         Sdk {
     48             code: String,
     49             message: String,
     50             retryable: bool,
     51         },
     52     }
     53 
     54     impl From<radroots_sdk::Error> for Error {
     55         fn from(error: radroots_sdk::Error) -> Self {
     56             let descriptor = error.descriptor();
     57             Self::Sdk {
     58                 code: descriptor.code().as_str().to_owned(),
     59                 message: descriptor.message().to_owned(),
     60                 retryable: descriptor.retryable(),
     61             }
     62         }
     63     }
     64 
     65     impl Error {
     66         fn contract(code: &'static str, message: &'static str) -> Self {
     67             Self::Sdk {
     68                 code: code.to_owned(),
     69                 message: message.to_owned(),
     70                 retryable: false,
     71             }
     72         }
     73     }
     74 
     75     /// Final evidence coverage vocabulary.
     76     #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
     77     pub enum TradeEvidenceCoverage {
     78         Missing,
     79         Partial,
     80         ScopeSatisfied,
     81         Unsupported,
     82     }
     83 
     84     /// Final evidence outcome vocabulary.
     85     #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
     86     pub enum TradeEvidenceOutcome {
     87         Valid,
     88         Invalid,
     89         Indeterminate,
     90     }
     91 
     92     /// Bounded projection of one canonical evidence manifest.
     93     #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
     94     pub struct TradeEvidenceManifest {
     95         pub contract_id: String,
     96         pub contract_version: u16,
     97         pub trade_id: String,
     98         pub trade_generation: String,
     99         pub observed_at_unix_s: String,
    100         pub coverage: TradeEvidenceCoverage,
    101         pub evidence_policy_digest: String,
    102         pub manifest_digest: String,
    103         pub canonical_bytes_hex: String,
    104     }
    105 
    106     /// Exact immutable RHI supersession reference.
    107     #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
    108     pub struct RhiEvidenceSupersession {
    109         pub report_id: String,
    110         pub event_id: String,
    111     }
    112 
    113     /// Bounded projection of one canonical RHI evidence report.
    114     #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
    115     pub struct RhiEvidenceReport {
    116         pub contract_id: String,
    117         pub contract_version: u16,
    118         pub issuer_pubkey: String,
    119         pub trade_id: String,
    120         pub claim_mutation_id: String,
    121         pub outcome: TradeEvidenceOutcome,
    122         pub reason_codes: Vec<String>,
    123         pub projection_digest: String,
    124         pub evidence_manifest_digest: String,
    125         pub evidence_policy_digest: String,
    126         pub observed_at_unix_s: String,
    127         pub trade_generation: String,
    128         pub statement_digest: String,
    129         pub supersession: Option<RhiEvidenceSupersession>,
    130         pub canonical_content: String,
    131     }
    132 
    133     /// One unsigned typed event plan ready for host-owned signing.
    134     #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
    135     pub struct TypedEvidenceEventPlan {
    136         pub contract_id: String,
    137         pub kind: u32,
    138         pub author_pubkey: String,
    139         pub created_at_unix_s: String,
    140         pub expected_event_id: String,
    141         pub tags: Vec<Vec<String>>,
    142         pub content: String,
    143     }
    144 
    145     /// Signed NIP-01 event input for verified attestation admission.
    146     #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
    147     pub struct SignedEvent {
    148         pub id: String,
    149         pub author_pubkey: String,
    150         pub created_at_unix_s: u64,
    151         pub kind: u32,
    152         pub tags: Vec<Vec<String>>,
    153         pub content: String,
    154         pub signature: String,
    155     }
    156 
    157     /// Verified final RHI attestation projection.
    158     #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
    159     pub struct RhiEvidenceAttestation {
    160         pub issuer_pubkey: String,
    161         pub trade_id: String,
    162         pub claim_mutation_id: String,
    163         pub outcome: TradeEvidenceOutcome,
    164         pub observed_at_unix_s: String,
    165         pub trade_generation: String,
    166         pub statement_digest: String,
    167         pub supersession: Option<RhiEvidenceSupersession>,
    168         pub canonical_content: String,
    169     }
    170 
    171     #[uniffi::export]
    172     pub fn parse_trade_evidence_manifest(
    173         canonical_bytes: Vec<u8>,
    174     ) -> Result<TradeEvidenceManifest, Error> {
    175         let manifest =
    176             radroots_sdk::trade::parse_evidence_manifest(&canonical_bytes).map_err(|_| {
    177                 Error::contract("invalid_evidence_manifest", "evidence manifest is invalid")
    178             })?;
    179         Ok(TradeEvidenceManifest::from(&manifest))
    180     }
    181 
    182     #[uniffi::export]
    183     pub fn parse_rhi_evidence_report(
    184         canonical_content: String,
    185     ) -> Result<RhiEvidenceReport, Error> {
    186         let report = radroots_sdk::trade::parse_rhi_evidence_report(canonical_content.as_bytes())
    187             .map_err(|_| {
    188             Error::contract("invalid_evidence_report", "evidence report is invalid")
    189         })?;
    190         Ok(RhiEvidenceReport::from(&report))
    191     }
    192 
    193     #[uniffi::export]
    194     pub fn prepare_rhi_evidence_attestation(
    195         canonical_content: String,
    196         created_at_unix_s: u64,
    197     ) -> Result<TypedEvidenceEventPlan, Error> {
    198         let report = radroots_sdk::trade::parse_rhi_evidence_report(canonical_content.as_bytes())
    199             .map_err(|_| {
    200             Error::contract("invalid_evidence_report", "evidence report is invalid")
    201         })?;
    202         let plan =
    203             radroots_sdk::trade::prepare_rhi_evidence_attestation(&report, created_at_unix_s)
    204                 .map_err(|_| {
    205                     Error::contract(
    206                         "invalid_evidence_attestation_plan",
    207                         "evidence attestation plan is invalid",
    208                     )
    209                 })?;
    210         Ok(TypedEvidenceEventPlan::from(&plan))
    211     }
    212 
    213     #[uniffi::export]
    214     pub fn validate_rhi_evidence_attestation(
    215         event: SignedEvent,
    216     ) -> Result<RhiEvidenceAttestation, Error> {
    217         let event = radroots_event::envelope::EventEnvelope::new(
    218             radroots_event::envelope::EventEnvelopeParts {
    219                 id: event.id,
    220                 author: event.author_pubkey,
    221                 created_at: event.created_at_unix_s,
    222                 kind: event.kind,
    223                 tags: event.tags,
    224                 content: event.content,
    225                 sig: event.signature,
    226             },
    227         )
    228         .map_err(|_| Error::contract("invalid_signed_event", "signed event is invalid"))?;
    229         let attestation =
    230             radroots_sdk::trade::validate_rhi_evidence_attestation(event).map_err(|error| {
    231                 match error {
    232                     radroots_sdk::trade::EvidenceAttestationValidationError::Signature => {
    233                         Error::contract(
    234                             "invalid_event_signature",
    235                             "event signature validation failed",
    236                         )
    237                     }
    238                     radroots_sdk::trade::EvidenceAttestationValidationError::Contract => {
    239                         Error::contract(
    240                             "invalid_evidence_attestation",
    241                             "evidence attestation is invalid",
    242                         )
    243                     }
    244                 }
    245             })?;
    246         Ok(RhiEvidenceAttestation::from(&attestation))
    247     }
    248 
    249     impl From<&radroots_sdk::trade::RadrootsTradeEvidenceManifestV1> for TradeEvidenceManifest {
    250         fn from(value: &radroots_sdk::trade::RadrootsTradeEvidenceManifestV1) -> Self {
    251             Self {
    252                 contract_id: value.contract_id().to_owned(),
    253                 contract_version: value.contract_version(),
    254                 trade_id: value.trade_id().to_string(),
    255                 trade_generation: value.trade_generation().get().to_string(),
    256                 observed_at_unix_s: value.observed_at_unix_s().to_string(),
    257                 coverage: value.coverage().into(),
    258                 evidence_policy_digest: value.evidence_policy_digest().to_hex(),
    259                 manifest_digest: value.digest().to_hex(),
    260                 canonical_bytes_hex: hex::encode(value.canonical_bytes()),
    261             }
    262         }
    263     }
    264 
    265     impl From<radroots_sdk::trade::RadrootsTradeEvidenceCoverageV1> for TradeEvidenceCoverage {
    266         fn from(value: radroots_sdk::trade::RadrootsTradeEvidenceCoverageV1) -> Self {
    267             match value {
    268                 radroots_sdk::trade::RadrootsTradeEvidenceCoverageV1::Missing => Self::Missing,
    269                 radroots_sdk::trade::RadrootsTradeEvidenceCoverageV1::Partial => Self::Partial,
    270                 radroots_sdk::trade::RadrootsTradeEvidenceCoverageV1::ScopeSatisfied => {
    271                     Self::ScopeSatisfied
    272                 }
    273                 radroots_sdk::trade::RadrootsTradeEvidenceCoverageV1::Unsupported => {
    274                     Self::Unsupported
    275                 }
    276             }
    277         }
    278     }
    279 
    280     impl From<radroots_sdk::trade::RadrootsTradeEvidenceOutcomeV1> for TradeEvidenceOutcome {
    281         fn from(value: radroots_sdk::trade::RadrootsTradeEvidenceOutcomeV1) -> Self {
    282             match value {
    283                 radroots_sdk::trade::RadrootsTradeEvidenceOutcomeV1::Valid => Self::Valid,
    284                 radroots_sdk::trade::RadrootsTradeEvidenceOutcomeV1::Invalid => Self::Invalid,
    285                 radroots_sdk::trade::RadrootsTradeEvidenceOutcomeV1::Indeterminate => {
    286                     Self::Indeterminate
    287                 }
    288             }
    289         }
    290     }
    291 
    292     impl From<&radroots_sdk::trade::RadrootsRhiEvidenceReportV1> for RhiEvidenceReport {
    293         fn from(value: &radroots_sdk::trade::RadrootsRhiEvidenceReportV1) -> Self {
    294             Self {
    295                 contract_id: value.contract_id().to_owned(),
    296                 contract_version: value.contract_version(),
    297                 issuer_pubkey: value.issuer_public_key().to_hex(),
    298                 trade_id: value.trade_id().to_string(),
    299                 claim_mutation_id: value.claim_mutation_id().to_string(),
    300                 outcome: value.outcome().into(),
    301                 reason_codes: value
    302                     .reason_codes()
    303                     .iter()
    304                     .map(|code| code.as_str().to_owned())
    305                     .collect(),
    306                 projection_digest: value.projection_digest().to_hex(),
    307                 evidence_manifest_digest: value.evidence_manifest_digest().to_hex(),
    308                 evidence_policy_digest: value.evidence_policy_digest().to_hex(),
    309                 observed_at_unix_s: value.observed_at_unix_s().to_string(),
    310                 trade_generation: value.trade_generation().get().to_string(),
    311                 statement_digest: value.statement_digest().to_hex(),
    312                 supersession: value.supersession().map(RhiEvidenceSupersession::from),
    313                 canonical_content: value.canonical_content().to_owned(),
    314             }
    315         }
    316     }
    317 
    318     impl From<radroots_sdk::trade::RadrootsRhiEvidenceSupersessionV1> for RhiEvidenceSupersession {
    319         fn from(value: radroots_sdk::trade::RadrootsRhiEvidenceSupersessionV1) -> Self {
    320             Self {
    321                 report_id: value.report_id().to_hex(),
    322                 event_id: value.event_id().to_hex(),
    323             }
    324         }
    325     }
    326 
    327     impl From<&radroots_event_codec::authoring::AuthoredEventPlan> for TypedEvidenceEventPlan {
    328         fn from(value: &radroots_event_codec::authoring::AuthoredEventPlan) -> Self {
    329             Self {
    330                 contract_id: value.body().contract().contract_id().as_str().to_owned(),
    331                 kind: value.body().kind(),
    332                 author_pubkey: value.author().to_hex(),
    333                 created_at_unix_s: value.created_at().to_string(),
    334                 expected_event_id: value.expected_event_id().to_hex(),
    335                 tags: value.body().tags().to_vec(),
    336                 content: value.body().content().to_owned(),
    337             }
    338         }
    339     }
    340 
    341     impl From<&radroots_sdk::trade::RadrootsRhiEvidenceAttestationV1> for RhiEvidenceAttestation {
    342         fn from(value: &radroots_sdk::trade::RadrootsRhiEvidenceAttestationV1) -> Self {
    343             Self {
    344                 issuer_pubkey: value.issuer().to_hex(),
    345                 trade_id: value.trade_id().to_string(),
    346                 claim_mutation_id: value.claim_mutation_id().to_string(),
    347                 outcome: match value.outcome() {
    348                     radroots_sdk::trade::RadrootsRhiEvidenceAttestationOutcomeV1::Valid => {
    349                         TradeEvidenceOutcome::Valid
    350                     }
    351                     radroots_sdk::trade::RadrootsRhiEvidenceAttestationOutcomeV1::Invalid => {
    352                         TradeEvidenceOutcome::Invalid
    353                     }
    354                     radroots_sdk::trade::RadrootsRhiEvidenceAttestationOutcomeV1::Indeterminate => {
    355                         TradeEvidenceOutcome::Indeterminate
    356                     }
    357                 },
    358                 observed_at_unix_s: value.observed_at_unix_s().to_string(),
    359                 trade_generation: value.trade_generation().get().to_string(),
    360                 statement_digest: hex::encode(value.statement_digest()),
    361                 supersession: value
    362                     .supersession()
    363                     .map(|supersession| RhiEvidenceSupersession {
    364                         report_id: hex::encode(supersession.report_id()),
    365                         event_id: supersession.event_id().to_hex(),
    366                     }),
    367                 canonical_content: value.canonical_content().to_owned(),
    368             }
    369         }
    370     }
    371 
    372     /// Thread-safe mobile handle delegating lifecycle to [`radroots_sdk::Client`].
    373     #[derive(uniffi::Object)]
    374     pub struct MobileClient {
    375         client: Client,
    376     }
    377 
    378     #[uniffi::export]
    379     impl MobileClient {
    380         /// Creates a deterministic, local-only memory client without I/O.
    381         #[uniffi::constructor]
    382         pub fn memory() -> Result<Arc<Self>, Error> {
    383             let client = ClientBuilder::memory_default().build()?;
    384             Ok(Arc::new(Self { client }))
    385         }
    386 
    387         /// Returns stable capability DTOs without probing host resources.
    388         pub fn capabilities(&self) -> Vec<CapabilityStatus> {
    389             self.client
    390                 .capabilities()
    391                 .iter()
    392                 .copied()
    393                 .map(CapabilityStatus::from)
    394                 .collect()
    395         }
    396 
    397         /// Returns whether explicit SDK close completed across all clones.
    398         pub fn is_closed(&self) -> bool {
    399             self.client.is_closed()
    400         }
    401 
    402         /// Explicitly closes SDK-owned resources without installing a runtime.
    403         pub async fn close(&self) -> Result<(), Error> {
    404             self.client.close().await.map_err(Error::from)
    405         }
    406     }
    407 
    408     impl From<radroots_sdk::capability::CapabilityStatus> for CapabilityStatus {
    409         fn from(status: radroots_sdk::capability::CapabilityStatus) -> Self {
    410             Self {
    411                 id: status.id().as_str().to_owned(),
    412                 compiled: status.is_compiled(),
    413                 configured: status.is_configured(),
    414                 availability: match status.availability() {
    415                     radroots_sdk::capability::Availability::Available => {
    416                         CapabilityAvailability::Available
    417                     }
    418                     radroots_sdk::capability::Availability::Degraded => {
    419                         CapabilityAvailability::Degraded
    420                     }
    421                     radroots_sdk::capability::Availability::Unavailable => {
    422                         CapabilityAvailability::Unavailable
    423                     }
    424                     radroots_sdk::capability::Availability::Unsupported => {
    425                         CapabilityAvailability::Unsupported
    426                     }
    427                 },
    428                 maturity: match status.maturity() {
    429                     radroots_sdk::capability::Maturity::Stable => CapabilityMaturity::Stable,
    430                     radroots_sdk::capability::Maturity::Preview => CapabilityMaturity::Preview,
    431                     radroots_sdk::capability::Maturity::Experimental => {
    432                         CapabilityMaturity::Experimental
    433                     }
    434                 },
    435             }
    436         }
    437     }
    438 }
    439 
    440 #[cfg(test)]
    441 mod tests {
    442     use core::num::NonZeroU64;
    443 
    444     use radroots_event::id::TradeId;
    445 
    446     use super::v1::{
    447         CapabilityAvailability, Error, MobileClient, SignedEvent, TradeEvidenceCoverage,
    448         TradeEvidenceOutcome, parse_rhi_evidence_report, parse_trade_evidence_manifest,
    449         prepare_rhi_evidence_attestation, validate_rhi_evidence_attestation,
    450     };
    451 
    452     fn assert_send_sync<T: Send + Sync>() {}
    453 
    454     #[tokio::test(flavor = "current_thread")]
    455     async fn memory_client_delegates_capabilities_and_lifecycle() {
    456         assert_send_sync::<MobileClient>();
    457         let client = MobileClient::memory().expect("memory client");
    458         let capabilities = client.capabilities();
    459         let storage = capabilities
    460             .iter()
    461             .find(|status| status.id == "storage.canonical")
    462             .expect("canonical storage");
    463         assert!(storage.compiled);
    464         assert!(storage.configured);
    465         assert_eq!(storage.availability, CapabilityAvailability::Available);
    466         assert!(!client.is_closed());
    467 
    468         client.close().await.expect("close");
    469         assert!(client.is_closed());
    470         client.close().await.expect("idempotent close");
    471     }
    472 
    473     #[test]
    474     fn sdk_error_mapping_is_versioned_and_secret_safe() {
    475         let native = radroots_sdk::ClientBuilder::new()
    476             .build()
    477             .expect_err("missing storage");
    478         let error = Error::from(native);
    479         let Error::Sdk {
    480             code,
    481             message,
    482             retryable,
    483         } = error;
    484         assert_eq!(code, "missing_storage");
    485         assert_eq!(message, "SDK storage capability is not configured");
    486         assert!(!retryable);
    487     }
    488 
    489     #[test]
    490     fn final_report_plan_and_signed_event_cross_the_ffi_without_numeric_narrowing() {
    491         let fixture: serde_json::Value = serde_json::from_str(include_str!(
    492             "../../../contracts/conformance/vectors/event/authored_operations.v1.json"
    493         ))
    494         .expect("authored corpus");
    495         let expected = fixture["vectors"]
    496             .as_array()
    497             .expect("operations")
    498             .iter()
    499             .find(|entry| entry["id"] == "typed_rhi_evidence_attestation_017")
    500             .expect("RHI operation")
    501             .get("expected")
    502             .expect("expected");
    503         let content = expected["content"].as_str().expect("content").to_owned();
    504 
    505         let report = parse_rhi_evidence_report(content.clone()).expect("report");
    506         assert_eq!(report.outcome, TradeEvidenceOutcome::Indeterminate);
    507         assert_eq!(report.trade_generation, "7");
    508         assert_eq!(report.observed_at_unix_s, "1800000000");
    509         assert_eq!(report.canonical_content, content);
    510 
    511         let plan = prepare_rhi_evidence_attestation(content, 1_784_347_200).expect("plan");
    512         assert_eq!(plan.kind, 3_441);
    513         assert_eq!(plan.created_at_unix_s, "1784347200");
    514         assert_eq!(
    515             plan.expected_event_id,
    516             expected["event_id"].as_str().expect("expected event id")
    517         );
    518 
    519         let raw: serde_json::Value =
    520             serde_json::from_str(expected["raw_json"].as_str().expect("raw event"))
    521                 .expect("raw event JSON");
    522         let signed = SignedEvent {
    523             id: raw["id"].as_str().expect("id").to_owned(),
    524             author_pubkey: raw["pubkey"].as_str().expect("pubkey").to_owned(),
    525             created_at_unix_s: raw["created_at"].as_u64().expect("created_at"),
    526             kind: u32::try_from(raw["kind"].as_u64().expect("kind")).expect("u32 kind"),
    527             tags: serde_json::from_value(raw["tags"].clone()).expect("tags"),
    528             content: raw["content"].as_str().expect("content").to_owned(),
    529             signature: raw["sig"].as_str().expect("signature").to_owned(),
    530         };
    531         let attestation = validate_rhi_evidence_attestation(signed).expect("signed attestation");
    532         assert_eq!(attestation.outcome, TradeEvidenceOutcome::Indeterminate);
    533         assert_eq!(attestation.trade_generation, "7");
    534     }
    535 
    536     #[test]
    537     fn manifest_supersession_and_closed_evidence_vocabularies_cross_the_ffi() {
    538         use radroots_sdk::trade::{
    539             RadrootsTradeEvidenceManifestSourceResultV1, RadrootsTradeEvidenceManifestV1,
    540             RadrootsTradeEvidencePolicyDigestV1, RadrootsTradeEvidenceScopePrerequisitesV1,
    541             RadrootsTradeEvidenceSourceCompletionV1, RadrootsTradeEvidenceSourceIdV1,
    542             RadrootsTradeEvidenceSourceRequirementV1, RadrootsTradeEvidenceSourceResultDigestV1,
    543             RadrootsTradeEvidenceSourceResultV1,
    544         };
    545 
    546         for (source, expected) in [
    547             (
    548                 radroots_sdk::trade::RadrootsTradeEvidenceCoverageV1::Missing,
    549                 TradeEvidenceCoverage::Missing,
    550             ),
    551             (
    552                 radroots_sdk::trade::RadrootsTradeEvidenceCoverageV1::Partial,
    553                 TradeEvidenceCoverage::Partial,
    554             ),
    555             (
    556                 radroots_sdk::trade::RadrootsTradeEvidenceCoverageV1::ScopeSatisfied,
    557                 TradeEvidenceCoverage::ScopeSatisfied,
    558             ),
    559             (
    560                 radroots_sdk::trade::RadrootsTradeEvidenceCoverageV1::Unsupported,
    561                 TradeEvidenceCoverage::Unsupported,
    562             ),
    563         ] {
    564             assert_eq!(TradeEvidenceCoverage::from(source), expected);
    565         }
    566         for (source, expected) in [
    567             (
    568                 radroots_sdk::trade::RadrootsTradeEvidenceOutcomeV1::Valid,
    569                 TradeEvidenceOutcome::Valid,
    570             ),
    571             (
    572                 radroots_sdk::trade::RadrootsTradeEvidenceOutcomeV1::Invalid,
    573                 TradeEvidenceOutcome::Invalid,
    574             ),
    575             (
    576                 radroots_sdk::trade::RadrootsTradeEvidenceOutcomeV1::Indeterminate,
    577                 TradeEvidenceOutcome::Indeterminate,
    578             ),
    579         ] {
    580             assert_eq!(TradeEvidenceOutcome::from(source), expected);
    581         }
    582 
    583         let source = RadrootsTradeEvidenceManifestSourceResultV1::new(
    584             RadrootsTradeEvidenceSourceIdV1::parse("typed_source").expect("source id"),
    585             RadrootsTradeEvidenceSourceResultV1::new(
    586                 RadrootsTradeEvidenceSourceRequirementV1::Required,
    587                 RadrootsTradeEvidenceSourceCompletionV1::Complete,
    588                 0,
    589             )
    590             .expect("source result"),
    591             RadrootsTradeEvidenceSourceResultDigestV1::from_bytes([0x33; 32]),
    592         );
    593         let manifest = RadrootsTradeEvidenceManifestV1::new(
    594             TradeId::from_bytes([0x11; 16]),
    595             NonZeroU64::new(1).expect("nonzero generation"),
    596             RadrootsTradeEvidencePolicyDigestV1::from_bytes([0x22; 32]),
    597             1_800_000_000,
    598             RadrootsTradeEvidenceScopePrerequisitesV1::Satisfied,
    599             [source],
    600             [],
    601         )
    602         .expect("manifest");
    603         let projected = parse_trade_evidence_manifest(manifest.canonical_bytes().to_vec())
    604             .expect("manifest projection");
    605         assert_eq!(projected.coverage, TradeEvidenceCoverage::ScopeSatisfied);
    606         assert_eq!(
    607             projected.canonical_bytes_hex,
    608             hex::encode(manifest.canonical_bytes())
    609         );
    610         assert!(parse_trade_evidence_manifest(Vec::new()).is_err());
    611 
    612         let decisions: serde_json::Value = serde_json::from_str(include_str!(
    613             "../../../contracts/conformance/vectors/rhi/evidence_attestation_decision.v1.json"
    614         ))
    615         .expect("RHI decision corpus");
    616         let superseding = decisions["vectors"]
    617             .as_array()
    618             .expect("RHI decision vectors")
    619             .iter()
    620             .find(|entry| entry["id"] == "rhi_evidence_attestation_superseding_002")
    621             .expect("superseding vector");
    622         let report = parse_rhi_evidence_report(
    623             superseding["expected"]["canonical_event_content_utf8"]
    624                 .as_str()
    625                 .expect("canonical event content")
    626                 .to_owned(),
    627         )
    628         .expect("superseding report");
    629         assert_eq!(report.outcome, TradeEvidenceOutcome::Valid);
    630         let supersession = report.supersession.expect("supersession");
    631         assert_eq!(
    632             supersession.report_id,
    633             "7777777777777777777777777777777777777777777777777777777777777777"
    634         );
    635         assert_eq!(
    636             supersession.event_id,
    637             "8888888888888888888888888888888888888888888888888888888888888888"
    638         );
    639     }
    640 
    641     #[test]
    642     fn evidence_entry_points_classify_pre_plan_and_signed_event_failures() {
    643         assert!(prepare_rhi_evidence_attestation("invalid".to_owned(), 0).is_err());
    644         assert!(
    645             validate_rhi_evidence_attestation(SignedEvent {
    646                 id: String::new(),
    647                 author_pubkey: String::new(),
    648                 created_at_unix_s: 0,
    649                 kind: 0,
    650                 tags: Vec::new(),
    651                 content: String::new(),
    652                 signature: String::new(),
    653             })
    654             .is_err()
    655         );
    656     }
    657 
    658     #[test]
    659     fn ffi_contract_errors_are_bounded_and_secret_safe() {
    660         let secret = "private-report-content";
    661         let error = parse_rhi_evidence_report(secret.to_owned()).expect_err("malformed report");
    662         let display = error.to_string();
    663         let debug = format!("{error:?}");
    664         assert!(!display.contains(secret));
    665         assert!(!debug.contains(secret));
    666         assert!(display.contains("invalid_evidence_report"));
    667     }
    668 }