commit 3563f3b5a4331eb2cb3f925cafc9de524d844228
parent b6f024d271fdf6a6eb1cf815e85a6d225fe7d8e1
Author: triesap <tyson@radroots.org>
Date: Thu, 20 Aug 2026 07:04:10 +0000
bindings: expose evidence attestation contracts
- Expose final evidence manifest, report, and verified kind-3441 SDK adapters.
- Generate exact TypeScript, Wasm, UniFFI, and retained-mobile value surfaces.
- Keep manifest and signed-event admission bounded, strict, and secret-safe.
- Refresh dependency locks, inventories, fixtures, and the reviewed SDK API baseline.
Diffstat:
20 files changed, 1484 insertions(+), 16 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -3107,12 +3107,14 @@ dependencies = [
name = "radroots_event_codec_wasm"
version = "0.1.0-alpha"
dependencies = [
+ "hex",
"nostr 0.44.7",
"radroots_blossom",
"radroots_core",
"radroots_event",
"radroots_event_codec",
"radroots_identity",
+ "radroots_trade",
"serde",
"serde_json",
"wasm-bindgen",
@@ -3453,7 +3455,11 @@ dependencies = [
name = "radroots_sdk_ffi"
version = "0.1.0-alpha"
dependencies = [
+ "hex",
+ "radroots_event",
+ "radroots_event_codec",
"radroots_sdk",
+ "serde_json",
"thiserror 1.0.69",
"tokio",
"uniffi",
diff --git a/contracts/api_baselines/radroots_sdk.txt b/contracts/api_baselines/radroots_sdk.txt
@@ -375,6 +375,40 @@ pub async fn radroots_sdk::sync::Operations<'a>::submit_push(&self, radroots_syn
impl core::fmt::Debug for radroots_sdk::sync::Operations<'_>
pub fn radroots_sdk::sync::Operations<'_>::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
pub mod radroots_sdk::trade
+pub use radroots_sdk::trade::RadrootsRhiEvidenceAttestationError
+pub use radroots_sdk::trade::RadrootsRhiEvidenceAttestationOutcomeV1
+pub use radroots_sdk::trade::RadrootsRhiEvidenceAttestationSupersessionV1
+pub use radroots_sdk::trade::RadrootsRhiEvidenceAttestationV1
+pub use radroots_sdk::trade::RadrootsRhiEvidenceReasonCodeV1
+pub use radroots_sdk::trade::RadrootsRhiEvidenceReportError
+pub use radroots_sdk::trade::RadrootsRhiEvidenceReportV1
+pub use radroots_sdk::trade::RadrootsRhiEvidenceStatementDigestV1
+pub use radroots_sdk::trade::RadrootsRhiEvidenceSupersessionV1
+pub use radroots_sdk::trade::RadrootsTradeEvidenceCoverageError
+pub use radroots_sdk::trade::RadrootsTradeEvidenceCoverageV1
+pub use radroots_sdk::trade::RadrootsTradeEvidenceManifestDigestV1
+pub use radroots_sdk::trade::RadrootsTradeEvidenceManifestError
+pub use radroots_sdk::trade::RadrootsTradeEvidenceManifestObservationV1
+pub use radroots_sdk::trade::RadrootsTradeEvidenceManifestSourceResultV1
+pub use radroots_sdk::trade::RadrootsTradeEvidenceManifestV1
+pub use radroots_sdk::trade::RadrootsTradeEvidenceOutcomeV1
+pub use radroots_sdk::trade::RadrootsTradeEvidencePolicyDigestV1
+pub use radroots_sdk::trade::RadrootsTradeEvidenceProjectionDigestV1
+pub use radroots_sdk::trade::RadrootsTradeEvidenceProvenanceDigestV1
+pub use radroots_sdk::trade::RadrootsTradeEvidenceScopePrerequisitesV1
+pub use radroots_sdk::trade::RadrootsTradeEvidenceSourceCompletionV1
+pub use radroots_sdk::trade::RadrootsTradeEvidenceSourceIdV1
+pub use radroots_sdk::trade::RadrootsTradeEvidenceSourceRequirementV1
+pub use radroots_sdk::trade::RadrootsTradeEvidenceSourceResultDigestV1
+pub use radroots_sdk::trade::RadrootsTradeEvidenceSourceResultV1
+pub use radroots_sdk::trade::RadrootsTradeSignedEventDigestV1
+pub use radroots_sdk::trade::classify_trade_evidence_coverage_v1
+pub enum radroots_sdk::trade::EvidenceAttestationValidationError
+pub radroots_sdk::trade::EvidenceAttestationValidationError::Contract
+pub radroots_sdk::trade::EvidenceAttestationValidationError::Signature
+impl core::error::Error for radroots_sdk::trade::EvidenceAttestationValidationError
+impl core::fmt::Display for radroots_sdk::trade::EvidenceAttestationValidationError
+pub fn radroots_sdk::trade::EvidenceAttestationValidationError::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
#[non_exhaustive] pub enum radroots_sdk::trade::PrepareErrorKind
pub radroots_sdk::trade::PrepareErrorKind::CanonicalMutation
pub radroots_sdk::trade::PrepareErrorKind::Draft
@@ -413,8 +447,12 @@ pub fn radroots_sdk::trade::PrepareError::fmt(&self, &mut core::fmt::Formatter<'
pub struct radroots_sdk::trade::PrepareRequest
impl radroots_sdk::trade::PrepareRequest
pub const fn radroots_sdk::trade::PrepareRequest::new(radroots_signing::actor::Actor, radroots_event::trade::TradeMutationEnvelopeV1) -> Self
+pub fn radroots_sdk::trade::parse_evidence_manifest(&[u8]) -> core::result::Result<radroots_trade::evidence_manifest::RadrootsTradeEvidenceManifestV1, radroots_trade::evidence_manifest::RadrootsTradeEvidenceManifestError>
+pub fn radroots_sdk::trade::parse_rhi_evidence_report(&[u8]) -> core::result::Result<radroots_trade::evidence_report::RadrootsRhiEvidenceReportV1, radroots_trade::evidence_report::RadrootsRhiEvidenceReportError>
pub fn radroots_sdk::trade::prepare(radroots_sdk::trade::PrepareRequest) -> core::result::Result<radroots_sdk::trade::Plan, radroots_sdk::trade::PrepareError>
+pub fn radroots_sdk::trade::prepare_rhi_evidence_attestation(&radroots_trade::evidence_report::RadrootsRhiEvidenceReportV1, u64) -> core::result::Result<radroots_event_codec::authoring::AuthoredEventPlan, radroots_event_codec::authoring::typed::AuthoredPlanError>
pub fn radroots_sdk::trade::project(radroots_trade::trade_contract_v1::RadrootsTradeReductionInputV1) -> radroots_trade::trade_contract_v1::RadrootsTradeProjectionV1
+pub fn radroots_sdk::trade::validate_rhi_evidence_attestation(radroots_event::envelope::EventEnvelope) -> core::result::Result<radroots_event_codec::rhi::RadrootsRhiEvidenceAttestationV1, radroots_sdk::trade::EvidenceAttestationValidationError>
pub mod radroots_sdk::transport
pub use radroots_sdk::transport::ReconnectBackoff
pub use radroots_sdk::transport::RelayAccess
diff --git a/crates/event_bindings/src/lib.rs b/crates/event_bindings/src/lib.rs
@@ -23,6 +23,8 @@ mod tests {
assert!(kinds_ts.contains(&kinds::KIND_CLASSIFIED_LISTING.to_string()));
assert!(kinds_ts.contains("KIND_TRADE_PROPOSAL"));
assert!(kinds_ts.contains(&kinds::KIND_TRADE_PROPOSAL.to_string()));
+ assert!(kinds_ts.contains("KIND_RHI_EVIDENCE_ATTESTATION"));
+ assert!(kinds_ts.contains(&kinds::KIND_RHI_EVIDENCE_ATTESTATION.to_string()));
assert!(!kinds_ts.contains("KIND_ORDER_REQUEST"));
}
}
diff --git a/crates/event_bindings/src/model.rs b/crates/event_bindings/src/model.rs
@@ -136,6 +136,10 @@ const EVENT_KIND_EXPORTS: &[(&str, u32)] = &[
"KIND_TRADE_VALIDATION_RECEIPT",
kinds::KIND_TRADE_VALIDATION_RECEIPT,
),
+ (
+ "KIND_RHI_EVIDENCE_ATTESTATION",
+ kinds::KIND_RHI_EVIDENCE_ATTESTATION,
+ ),
("KIND_KNOWLEDGE_CLAIM", kinds::KIND_KNOWLEDGE_CLAIM),
("KIND_KNOWLEDGE_RELATION", kinds::KIND_KNOWLEDGE_RELATION),
("KIND_KNOWLEDGE_REVIEW", kinds::KIND_KNOWLEDGE_REVIEW),
diff --git a/crates/event_bindings/tests/fixtures/generated_kinds.ts b/crates/event_bindings/tests/fixtures/generated_kinds.ts
@@ -122,6 +122,8 @@ export const KIND_TRADE_SELLER_RESERVATION_ASSERTION = 3475;
export const KIND_TRADE_VALIDATION_RECEIPT = 3440;
+export const KIND_RHI_EVIDENCE_ATTESTATION = 3441;
+
export const KIND_KNOWLEDGE_CLAIM = 3460;
export const KIND_KNOWLEDGE_RELATION = 3461;
diff --git a/crates/event_codec_wasm/Cargo.toml b/crates/event_codec_wasm/Cargo.toml
@@ -15,6 +15,7 @@ readme = "README"
crate-type = ["cdylib", "rlib"]
[dependencies]
+hex = { workspace = true }
radroots_blossom = { workspace = true, default-features = false, features = [
"std",
] }
@@ -27,6 +28,10 @@ radroots_event_codec = { workspace = true, default-features = false, features =
"std",
"manifests",
] }
+radroots_trade = { workspace = true, default-features = false, features = [
+ "json",
+ "std",
+] }
serde = { workspace = true }
serde_json = { workspace = true }
wasm-bindgen = { workspace = true }
diff --git a/crates/event_codec_wasm/src/lib.rs b/crates/event_codec_wasm/src/lib.rs
@@ -91,6 +91,18 @@ use radroots_event_codec::encode::report::report_build_tags;
use radroots_event_codec::encode::repost::{generic_repost_build_tags, repost_build_tags};
use radroots_event_codec::encode::seal::seal_build_tags;
use radroots_event_codec::verify::{RadrootsDecodeError, RadrootsDecodedEvent};
+use radroots_event_codec::{
+ authoring::AuthoredEventPlan,
+ decode::rhi::{
+ RadrootsRhiEvidenceAttestationOutcomeV1, RadrootsRhiEvidenceAttestationV1,
+ rhi_evidence_attestation_from_verified_event,
+ },
+ verify::{Nip01SignatureVerifier, id as verify_event_id, signature as verify_event_signature},
+};
+use radroots_trade::evidence::{
+ RadrootsRhiEvidenceReportV1, RadrootsTradeEvidenceCoverageV1, RadrootsTradeEvidenceManifestV1,
+ RadrootsTradeEvidenceOutcomeV1,
+};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
#[cfg(target_arch = "wasm32")]
@@ -164,6 +176,134 @@ fn tags_to_json(tags: Vec<Vec<String>>) -> Result<String, RadrootsJsValue> {
serde_json::to_string(&tags).map_err(err_js)
}
+#[derive(Serialize)]
+struct EvidenceManifestJson {
+ contract_id: &'static str,
+ contract_version: u16,
+ trade_id: String,
+ trade_generation: String,
+ observed_at_unix_s: String,
+ coverage: &'static str,
+ evidence_policy_digest: String,
+ manifest_digest: String,
+ canonical_bytes_hex: String,
+}
+
+#[derive(Serialize)]
+struct EvidenceSupersessionJson {
+ report_id: String,
+ event_id: String,
+}
+
+#[derive(Serialize)]
+struct EvidenceReportJson {
+ contract_id: &'static str,
+ contract_version: u16,
+ issuer_pubkey: String,
+ trade_id: String,
+ claim_mutation_id: String,
+ outcome: &'static str,
+ reason_codes: Vec<String>,
+ projection_digest: String,
+ evidence_manifest_digest: String,
+ evidence_policy_digest: String,
+ observed_at_unix_s: String,
+ trade_generation: String,
+ statement_digest: String,
+ supersession: Option<EvidenceSupersessionJson>,
+ canonical_content: String,
+}
+
+#[derive(Serialize)]
+struct TypedEvidenceEventPlanJson {
+ contract_id: String,
+ kind: u32,
+ author_pubkey: String,
+ created_at_unix_s: String,
+ expected_event_id: String,
+ tags: Vec<Vec<String>>,
+ content: String,
+}
+
+#[derive(Serialize)]
+struct EvidenceAttestationJson {
+ issuer_pubkey: String,
+ trade_id: String,
+ claim_mutation_id: String,
+ outcome: &'static str,
+ observed_at_unix_s: String,
+ trade_generation: String,
+ statement_digest: String,
+ supersession: Option<EvidenceSupersessionJson>,
+ canonical_content: String,
+}
+
+fn coverage_name(value: RadrootsTradeEvidenceCoverageV1) -> &'static str {
+ match value {
+ RadrootsTradeEvidenceCoverageV1::Missing => "missing",
+ RadrootsTradeEvidenceCoverageV1::Partial => "partial",
+ RadrootsTradeEvidenceCoverageV1::ScopeSatisfied => "scope_satisfied",
+ RadrootsTradeEvidenceCoverageV1::Unsupported => "unsupported",
+ }
+}
+
+fn outcome_name(value: RadrootsTradeEvidenceOutcomeV1) -> &'static str {
+ match value {
+ RadrootsTradeEvidenceOutcomeV1::Valid => "valid",
+ RadrootsTradeEvidenceOutcomeV1::Invalid => "invalid",
+ RadrootsTradeEvidenceOutcomeV1::Indeterminate => "indeterminate",
+ }
+}
+
+fn attestation_outcome_name(value: RadrootsRhiEvidenceAttestationOutcomeV1) -> &'static str {
+ match value {
+ RadrootsRhiEvidenceAttestationOutcomeV1::Valid => "valid",
+ RadrootsRhiEvidenceAttestationOutcomeV1::Invalid => "invalid",
+ RadrootsRhiEvidenceAttestationOutcomeV1::Indeterminate => "indeterminate",
+ }
+}
+
+fn evidence_report_json(value: &RadrootsRhiEvidenceReportV1) -> EvidenceReportJson {
+ EvidenceReportJson {
+ contract_id: value.contract_id(),
+ contract_version: value.contract_version(),
+ issuer_pubkey: value.issuer_public_key().to_hex(),
+ trade_id: value.trade_id().to_string(),
+ claim_mutation_id: value.claim_mutation_id().to_string(),
+ outcome: outcome_name(value.outcome()),
+ reason_codes: value
+ .reason_codes()
+ .iter()
+ .map(|code| code.as_str().to_owned())
+ .collect(),
+ projection_digest: value.projection_digest().to_hex(),
+ evidence_manifest_digest: value.evidence_manifest_digest().to_hex(),
+ evidence_policy_digest: value.evidence_policy_digest().to_hex(),
+ observed_at_unix_s: value.observed_at_unix_s().to_string(),
+ trade_generation: value.trade_generation().get().to_string(),
+ statement_digest: value.statement_digest().to_hex(),
+ supersession: value
+ .supersession()
+ .map(|supersession| EvidenceSupersessionJson {
+ report_id: supersession.report_id().to_hex(),
+ event_id: supersession.event_id().to_hex(),
+ }),
+ canonical_content: value.canonical_content().to_owned(),
+ }
+}
+
+fn typed_plan_json(value: &AuthoredEventPlan) -> TypedEvidenceEventPlanJson {
+ TypedEvidenceEventPlanJson {
+ contract_id: value.body().contract().contract_id().as_str().to_owned(),
+ kind: value.body().kind(),
+ author_pubkey: value.author().to_hex(),
+ created_at_unix_s: value.created_at().to_string(),
+ expected_event_id: value.expected_event_id().to_hex(),
+ tags: value.body().tags().to_vec(),
+ content: value.body().content().to_owned(),
+ }
+}
+
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct EventEnvelopeInput {
@@ -210,6 +350,13 @@ fn parse_event_json(input: &str) -> Result<EventEnvelope, RadrootsJsValue> {
.map_err(|error| error_json("invalid_event", Some(envelope_error_code(&error))))
}
+fn parse_bounded_event_json(input: &str) -> Result<EventEnvelope, RadrootsJsValue> {
+ if input.len() > radroots_event::wire::v1::DEFAULT_RAW_JSON_MAX_BYTES {
+ return Err(error_json("invalid_event", Some("event_too_large")));
+ }
+ parse_event_json(input)
+}
+
fn build_tags_json<T, E, F>(input: &str, build: F) -> Result<String, RadrootsJsValue>
where
T: DeserializeOwned,
@@ -610,6 +757,107 @@ pub fn verify_and_decode_event_json(event_json: &str) -> Result<String, Radroots
decoded_event_to_json(decoded)
}
+/// Parses one bounded canonical evidence manifest supplied as lowercase hex.
+#[cfg_attr(
+ target_arch = "wasm32",
+ wasm_bindgen(js_name = trade_evidence_manifest_parse_json)
+)]
+pub fn trade_evidence_manifest_parse_json(
+ canonical_bytes_hex: &str,
+) -> Result<String, RadrootsJsValue> {
+ if canonical_bytes_hex.len()
+ > radroots_trade::evidence::RADROOTS_TRADE_EVIDENCE_MANIFEST_MAXIMUM_BYTES * 2
+ || !canonical_bytes_hex.len().is_multiple_of(2)
+ || !canonical_bytes_hex
+ .bytes()
+ .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
+ {
+ return Err(error_json("invalid_evidence_manifest", None));
+ }
+ let canonical_bytes = hex::decode(canonical_bytes_hex)
+ .map_err(|_| error_json("invalid_evidence_manifest", None))?;
+ let manifest = RadrootsTradeEvidenceManifestV1::from_canonical_bytes(&canonical_bytes)
+ .map_err(|_| error_json("invalid_evidence_manifest", None))?;
+ serde_json::to_string(&EvidenceManifestJson {
+ contract_id: manifest.contract_id(),
+ contract_version: manifest.contract_version(),
+ trade_id: manifest.trade_id().to_string(),
+ trade_generation: manifest.trade_generation().get().to_string(),
+ observed_at_unix_s: manifest.observed_at_unix_s().to_string(),
+ coverage: coverage_name(manifest.coverage()),
+ evidence_policy_digest: manifest.evidence_policy_digest().to_hex(),
+ manifest_digest: manifest.digest().to_hex(),
+ canonical_bytes_hex: hex::encode(manifest.canonical_bytes()),
+ })
+ .map_err(|_| error_json("internal_error", Some("evidence_manifest_serialization")))
+}
+
+/// Parses one bounded canonical RHI evidence report.
+#[cfg_attr(
+ target_arch = "wasm32",
+ wasm_bindgen(js_name = rhi_evidence_report_parse_json)
+)]
+pub fn rhi_evidence_report_parse_json(canonical_content: &str) -> Result<String, RadrootsJsValue> {
+ let report = RadrootsRhiEvidenceReportV1::from_canonical_content(canonical_content.as_bytes())
+ .map_err(|_| error_json("invalid_evidence_report", None))?;
+ serde_json::to_string(&evidence_report_json(&report))
+ .map_err(|_| error_json("internal_error", Some("evidence_report_serialization")))
+}
+
+/// Builds one unsigned typed RHI evidence-attestation plan.
+#[cfg_attr(
+ target_arch = "wasm32",
+ wasm_bindgen(js_name = rhi_evidence_attestation_build_draft)
+)]
+pub fn rhi_evidence_attestation_build_draft(
+ canonical_content: &str,
+ created_at_unix_s: u64,
+) -> Result<String, RadrootsJsValue> {
+ let report = RadrootsRhiEvidenceReportV1::from_canonical_content(canonical_content.as_bytes())
+ .map_err(|_| error_json("invalid_evidence_report", None))?;
+ let attestation =
+ RadrootsRhiEvidenceAttestationV1::from_canonical_content(report.canonical_content())
+ .map_err(|_| error_json("invalid_evidence_attestation", None))?;
+ let plan = AuthoredEventPlan::from_rhi_evidence_attestation(&attestation, created_at_unix_s)
+ .map_err(|_| error_json("invalid_evidence_attestation_plan", None))?;
+ serde_json::to_string(&typed_plan_json(&plan))
+ .map_err(|_| error_json("internal_error", Some("evidence_plan_serialization")))
+}
+
+/// Verifies NIP-01 identity/signature before validating the RHI contract.
+#[cfg_attr(
+ target_arch = "wasm32",
+ wasm_bindgen(js_name = rhi_evidence_attestation_validate_signed_json)
+)]
+pub fn rhi_evidence_attestation_validate_signed_json(
+ event_json: &str,
+) -> Result<String, RadrootsJsValue> {
+ let event = parse_bounded_event_json(event_json)?;
+ let verified = verify_event_id(radroots_event::admission::RawEvent::new(event))
+ .and_then(|event| verify_event_signature(event, &Nip01SignatureVerifier))
+ .map_err(|_| error_json("invalid_event_signature", None))?;
+ let attestation = rhi_evidence_attestation_from_verified_event(&verified)
+ .map_err(|error| error_json("invalid_evidence_attestation", Some(error.code())))?;
+ let supersession = attestation
+ .supersession()
+ .map(|supersession| EvidenceSupersessionJson {
+ report_id: hex::encode(supersession.report_id()),
+ event_id: supersession.event_id().to_hex(),
+ });
+ serde_json::to_string(&EvidenceAttestationJson {
+ issuer_pubkey: attestation.issuer().to_hex(),
+ trade_id: attestation.trade_id().to_string(),
+ claim_mutation_id: attestation.claim_mutation_id().to_string(),
+ outcome: attestation_outcome_name(attestation.outcome()),
+ observed_at_unix_s: attestation.observed_at_unix_s().to_string(),
+ trade_generation: attestation.trade_generation().get().to_string(),
+ statement_digest: hex::encode(attestation.statement_digest()),
+ supersession,
+ canonical_content: attestation.canonical_content().to_owned(),
+ })
+ .map_err(|_| error_json("internal_error", Some("evidence_attestation_serialization")))
+}
+
#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = contract_manifest_json))]
pub fn contract_manifest_json() -> Result<String, RadrootsJsValue> {
radroots_event_codec::manifest::contract_manifest_json().map_err(err_js)
@@ -2179,4 +2427,68 @@ mod tests {
assert!(operational_listing_tags(&listing_json).is_err());
assert!(operational_listing_tags_full(&listing_json).is_err());
}
+
+ #[test]
+ fn evidence_report_plan_and_signed_validation_use_final_contracts() {
+ assert!(trade_evidence_manifest_parse_json("AA").is_err());
+ assert!(
+ rhi_evidence_attestation_validate_signed_json(
+ &"x".repeat(radroots_event::wire::v1::DEFAULT_RAW_JSON_MAX_BYTES + 1)
+ )
+ .is_err()
+ );
+
+ let fixture: serde_json::Value = serde_json::from_str(include_str!(
+ "../../../contracts/conformance/vectors/event/authored_operations.v1.json"
+ ))
+ .expect("authored corpus");
+ let expected = fixture["vectors"]
+ .as_array()
+ .expect("operations")
+ .iter()
+ .find(|entry| entry["id"] == "typed_rhi_evidence_attestation_017")
+ .expect("RHI operation")
+ .get("expected")
+ .expect("expected");
+ let content = expected["content"].as_str().expect("content");
+
+ let report: serde_json::Value = serde_json::from_str(
+ &rhi_evidence_report_parse_json(content).expect("report projection"),
+ )
+ .expect("report JSON");
+ assert_eq!(report["outcome"], "indeterminate");
+ assert_eq!(report["trade_generation"], "7");
+
+ let plan: serde_json::Value = serde_json::from_str(
+ &rhi_evidence_attestation_build_draft(content, 1_784_347_200).expect("typed plan"),
+ )
+ .expect("plan JSON");
+ assert_eq!(plan["kind"], 3_441);
+ assert_eq!(plan["created_at_unix_s"], "1784347200");
+ assert_eq!(plan["expected_event_id"], expected["event_id"]);
+
+ let raw: serde_json::Value =
+ serde_json::from_str(expected["raw_json"].as_str().expect("signed event"))
+ .expect("raw event JSON");
+ let boundary_event = serde_json::json!({
+ "id": raw["id"],
+ "author": raw["pubkey"],
+ "created_at": raw["created_at"],
+ "kind": raw["kind"],
+ "tags": raw["tags"],
+ "content": raw["content"],
+ "sig": raw["sig"],
+ });
+ let attestation: serde_json::Value = serde_json::from_str(
+ &rhi_evidence_attestation_validate_signed_json(&boundary_event.to_string())
+ .expect("verified attestation"),
+ )
+ .expect("attestation JSON");
+ assert_eq!(attestation["outcome"], "indeterminate");
+ assert_eq!(attestation["trade_generation"], "7");
+
+ let secret = "private-report-content";
+ let error = rhi_evidence_report_parse_json(secret).expect_err("malformed report");
+ assert!(!format!("{error:?}").contains(secret));
+ }
}
diff --git a/crates/mobile_core/src/runtime/sdk.rs b/crates/mobile_core/src/runtime/sdk.rs
@@ -1,5 +1,10 @@
use radroots_sdk::capability::{Availability, Maturity};
+pub use radroots_sdk::trade::{
+ RadrootsRhiEvidenceReportV1, RadrootsTradeEvidenceCoverageV1, RadrootsTradeEvidenceManifestV1,
+ RadrootsTradeEvidenceOutcomeV1,
+};
+
use super::RadrootsRuntime;
#[cfg(feature = "mobile-social")]
use super::product_surface::{BlossomPreferences, RelayPreferences};
diff --git a/crates/mobile_core/tests/package_boundary.rs b/crates/mobile_core/tests/package_boundary.rs
@@ -78,3 +78,24 @@ fn production_runtime_requires_validated_sqlite_and_memory_is_test_only() {
assert!(RUNTIME.contains("#[cfg(test)]\n pub(crate) fn test_memory()"));
assert!(!RUNTIME.contains("pub fn new()"));
}
+
+#[test]
+fn mobile_core_reuses_the_final_sdk_evidence_vocabulary() {
+ for required in [
+ "RadrootsRhiEvidenceReportV1",
+ "RadrootsTradeEvidenceCoverageV1",
+ "RadrootsTradeEvidenceManifestV1",
+ "RadrootsTradeEvidenceOutcomeV1",
+ ] {
+ assert!(
+ SDK.contains(required),
+ "missing SDK evidence type `{required}`"
+ );
+ }
+ for forbidden in ["SecretKey", "sign_event", "publish_event", "tokio::spawn"] {
+ assert!(
+ !SDK.contains(forbidden),
+ "mobile evidence projection gained forbidden authority `{forbidden}`"
+ );
+ }
+}
diff --git a/crates/mobile_ffi/src/dto.rs b/crates/mobile_ffi/src/dto.rs
@@ -43,6 +43,260 @@ pub const MOBILE_FFI_SCHEMA_VERSION: u16 = 1;
const MEDIA_FILE_MAX_BYTES: u64 = 10 * 1024 * 1024;
const MEDIA_REFERENCE_MAX_BYTES: usize = 256;
+/// Final four-state trade-evidence coverage vocabulary.
+#[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
+pub enum FfiTradeEvidenceCoverage {
+ Missing,
+ Partial,
+ ScopeSatisfied,
+ Unsupported,
+}
+
+/// Final three-state trade-evidence outcome vocabulary.
+#[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
+pub enum FfiTradeEvidenceOutcome {
+ Valid,
+ Invalid,
+ Indeterminate,
+}
+
+/// Secret-free projection of one canonical evidence manifest.
+#[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
+pub struct FfiTradeEvidenceManifestRecord {
+ pub schema_version: u16,
+ pub contract_id: String,
+ pub contract_version: u16,
+ pub trade_id: String,
+ pub trade_generation: String,
+ pub observed_at_unix_s: String,
+ pub coverage: FfiTradeEvidenceCoverage,
+ pub evidence_policy_digest: String,
+ pub manifest_digest: String,
+ pub canonical_bytes_hex: String,
+}
+
+/// Secret-free projection of one canonical RHI evidence report.
+#[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
+pub struct FfiRhiEvidenceReportRecord {
+ pub schema_version: u16,
+ pub contract_id: String,
+ pub contract_version: u16,
+ pub issuer_pubkey: String,
+ pub trade_id: String,
+ pub claim_mutation_id: String,
+ pub outcome: FfiTradeEvidenceOutcome,
+ pub reason_codes: Vec<String>,
+ pub projection_digest: String,
+ pub evidence_manifest_digest: String,
+ pub evidence_policy_digest: String,
+ pub observed_at_unix_s: String,
+ pub trade_generation: String,
+ pub statement_digest: String,
+ pub supersedes_report_id: Option<String>,
+ pub supersedes_event_id: Option<String>,
+ pub canonical_content: String,
+}
+
+/// Unsigned typed event plan ready for host-owned signing.
+#[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
+pub struct FfiTypedEvidenceEventPlanRecord {
+ pub schema_version: u16,
+ pub contract_id: String,
+ pub kind: u32,
+ pub author_pubkey: String,
+ pub created_at_unix_s: String,
+ pub expected_event_id: String,
+ pub tags: Vec<Vec<String>>,
+ pub content: String,
+}
+
+/// Signed NIP-01 event input for verified attestation admission.
+#[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
+pub struct FfiSignedEvidenceEventRecord {
+ pub id: String,
+ pub author_pubkey: String,
+ pub created_at_unix_s: u64,
+ pub kind: u32,
+ pub tags: Vec<Vec<String>>,
+ pub content: String,
+ pub signature: String,
+}
+
+/// Secret-free supersession projection from one verified attestation.
+#[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
+pub struct FfiRhiEvidenceAttestationSupersessionRecord {
+ pub report_id: String,
+ pub event_id: String,
+}
+
+/// Verified final RHI evidence attestation.
+#[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
+pub struct FfiRhiEvidenceAttestationRecord {
+ pub schema_version: u16,
+ pub issuer_pubkey: String,
+ pub trade_id: String,
+ pub claim_mutation_id: String,
+ pub outcome: FfiTradeEvidenceOutcome,
+ pub observed_at_unix_s: String,
+ pub trade_generation: String,
+ pub statement_digest: String,
+ pub supersession: Option<FfiRhiEvidenceAttestationSupersessionRecord>,
+ pub canonical_content: String,
+}
+
+#[uniffi::export]
+pub fn parse_trade_evidence_manifest(
+ canonical_bytes: Vec<u8>,
+) -> Result<FfiTradeEvidenceManifestRecord, RadrootsAppError> {
+ let manifest = radroots_sdk::trade::parse_evidence_manifest(&canonical_bytes)
+ .map_err(|_| RadrootsAppError::invalid_argument("invalid_evidence_manifest"))?;
+ Ok(FfiTradeEvidenceManifestRecord {
+ schema_version: MOBILE_FFI_SCHEMA_VERSION,
+ contract_id: manifest.contract_id().to_owned(),
+ contract_version: manifest.contract_version(),
+ trade_id: manifest.trade_id().to_string(),
+ trade_generation: manifest.trade_generation().get().to_string(),
+ observed_at_unix_s: manifest.observed_at_unix_s().to_string(),
+ coverage: manifest.coverage().into(),
+ evidence_policy_digest: manifest.evidence_policy_digest().to_hex(),
+ manifest_digest: manifest.digest().to_hex(),
+ canonical_bytes_hex: hex::encode(manifest.canonical_bytes()),
+ })
+}
+
+#[uniffi::export]
+pub fn parse_rhi_evidence_report(
+ canonical_content: String,
+) -> Result<FfiRhiEvidenceReportRecord, RadrootsAppError> {
+ let report = radroots_sdk::trade::parse_rhi_evidence_report(canonical_content.as_bytes())
+ .map_err(|_| RadrootsAppError::invalid_argument("invalid_evidence_report"))?;
+ let supersession = report.supersession();
+ Ok(FfiRhiEvidenceReportRecord {
+ schema_version: MOBILE_FFI_SCHEMA_VERSION,
+ contract_id: report.contract_id().to_owned(),
+ contract_version: report.contract_version(),
+ issuer_pubkey: report.issuer_public_key().to_hex(),
+ trade_id: report.trade_id().to_string(),
+ claim_mutation_id: report.claim_mutation_id().to_string(),
+ outcome: report.outcome().into(),
+ reason_codes: report
+ .reason_codes()
+ .iter()
+ .map(|code| code.as_str().to_owned())
+ .collect(),
+ projection_digest: report.projection_digest().to_hex(),
+ evidence_manifest_digest: report.evidence_manifest_digest().to_hex(),
+ evidence_policy_digest: report.evidence_policy_digest().to_hex(),
+ observed_at_unix_s: report.observed_at_unix_s().to_string(),
+ trade_generation: report.trade_generation().get().to_string(),
+ statement_digest: report.statement_digest().to_hex(),
+ supersedes_report_id: supersession.map(|value| value.report_id().to_hex()),
+ supersedes_event_id: supersession.map(|value| value.event_id().to_hex()),
+ canonical_content: report.canonical_content().to_owned(),
+ })
+}
+
+#[uniffi::export]
+pub fn prepare_rhi_evidence_attestation(
+ canonical_content: String,
+ created_at_unix_s: u64,
+) -> Result<FfiTypedEvidenceEventPlanRecord, RadrootsAppError> {
+ let report = radroots_sdk::trade::parse_rhi_evidence_report(canonical_content.as_bytes())
+ .map_err(|_| RadrootsAppError::invalid_argument("invalid_evidence_report"))?;
+ let plan = radroots_sdk::trade::prepare_rhi_evidence_attestation(&report, created_at_unix_s)
+ .map_err(|_| RadrootsAppError::invalid_argument("invalid_evidence_attestation_plan"))?;
+ Ok(FfiTypedEvidenceEventPlanRecord {
+ schema_version: MOBILE_FFI_SCHEMA_VERSION,
+ contract_id: plan.body().contract().contract_id().as_str().to_owned(),
+ kind: plan.body().kind(),
+ author_pubkey: plan.author().to_hex(),
+ created_at_unix_s: plan.created_at().to_string(),
+ expected_event_id: plan.expected_event_id().to_hex(),
+ tags: plan.body().tags().to_vec(),
+ content: plan.body().content().to_owned(),
+ })
+}
+
+#[uniffi::export]
+pub fn validate_rhi_evidence_attestation(
+ event: FfiSignedEvidenceEventRecord,
+) -> Result<FfiRhiEvidenceAttestationRecord, RadrootsAppError> {
+ let event = radroots_event::envelope::EventEnvelope::new(
+ radroots_event::envelope::EventEnvelopeParts {
+ id: event.id,
+ author: event.author_pubkey,
+ created_at: event.created_at_unix_s,
+ kind: event.kind,
+ tags: event.tags,
+ content: event.content,
+ sig: event.signature,
+ },
+ )
+ .map_err(|_| RadrootsAppError::invalid_argument("invalid_signed_event"))?;
+ let attestation = radroots_sdk::trade::validate_rhi_evidence_attestation(event).map_err(
+ |error| match error {
+ radroots_sdk::trade::EvidenceAttestationValidationError::Signature => {
+ RadrootsAppError::invalid_argument("invalid_event_signature")
+ }
+ radroots_sdk::trade::EvidenceAttestationValidationError::Contract => {
+ RadrootsAppError::invalid_argument("invalid_evidence_attestation")
+ }
+ },
+ )?;
+ Ok(FfiRhiEvidenceAttestationRecord {
+ schema_version: MOBILE_FFI_SCHEMA_VERSION,
+ issuer_pubkey: attestation.issuer().to_hex(),
+ trade_id: attestation.trade_id().to_string(),
+ claim_mutation_id: attestation.claim_mutation_id().to_string(),
+ outcome: match attestation.outcome() {
+ radroots_sdk::trade::RadrootsRhiEvidenceAttestationOutcomeV1::Valid => {
+ FfiTradeEvidenceOutcome::Valid
+ }
+ radroots_sdk::trade::RadrootsRhiEvidenceAttestationOutcomeV1::Invalid => {
+ FfiTradeEvidenceOutcome::Invalid
+ }
+ radroots_sdk::trade::RadrootsRhiEvidenceAttestationOutcomeV1::Indeterminate => {
+ FfiTradeEvidenceOutcome::Indeterminate
+ }
+ },
+ observed_at_unix_s: attestation.observed_at_unix_s().to_string(),
+ trade_generation: attestation.trade_generation().get().to_string(),
+ statement_digest: hex::encode(attestation.statement_digest()),
+ supersession: attestation.supersession().map(|value| {
+ FfiRhiEvidenceAttestationSupersessionRecord {
+ report_id: hex::encode(value.report_id()),
+ event_id: value.event_id().to_hex(),
+ }
+ }),
+ canonical_content: attestation.canonical_content().to_owned(),
+ })
+}
+
+impl From<radroots_sdk::trade::RadrootsTradeEvidenceCoverageV1> for FfiTradeEvidenceCoverage {
+ fn from(value: radroots_sdk::trade::RadrootsTradeEvidenceCoverageV1) -> Self {
+ match value {
+ radroots_sdk::trade::RadrootsTradeEvidenceCoverageV1::Missing => Self::Missing,
+ radroots_sdk::trade::RadrootsTradeEvidenceCoverageV1::Partial => Self::Partial,
+ radroots_sdk::trade::RadrootsTradeEvidenceCoverageV1::ScopeSatisfied => {
+ Self::ScopeSatisfied
+ }
+ radroots_sdk::trade::RadrootsTradeEvidenceCoverageV1::Unsupported => Self::Unsupported,
+ }
+ }
+}
+
+impl From<radroots_sdk::trade::RadrootsTradeEvidenceOutcomeV1> for FfiTradeEvidenceOutcome {
+ fn from(value: radroots_sdk::trade::RadrootsTradeEvidenceOutcomeV1) -> Self {
+ match value {
+ radroots_sdk::trade::RadrootsTradeEvidenceOutcomeV1::Valid => Self::Valid,
+ radroots_sdk::trade::RadrootsTradeEvidenceOutcomeV1::Invalid => Self::Invalid,
+ radroots_sdk::trade::RadrootsTradeEvidenceOutcomeV1::Indeterminate => {
+ Self::Indeterminate
+ }
+ }
+ }
+}
+
#[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
pub struct FfiBuildInfoRecord {
pub schema_version: u16,
@@ -2049,6 +2303,64 @@ mod tests {
use super::*;
+ fn rhi_attestation_fixture() -> serde_json::Value {
+ let fixture: serde_json::Value = serde_json::from_str(include_str!(
+ "../../../contracts/conformance/vectors/event/authored_operations.v1.json"
+ ))
+ .expect("authored corpus");
+ fixture["vectors"]
+ .as_array()
+ .expect("operations")
+ .iter()
+ .find(|entry| entry["id"] == "typed_rhi_evidence_attestation_017")
+ .expect("RHI operation")
+ .get("expected")
+ .expect("expected")
+ .clone()
+ }
+
+ #[test]
+ fn evidence_report_plan_and_verified_event_use_final_mobile_vocabulary() {
+ let expected = rhi_attestation_fixture();
+ let content = expected["content"].as_str().expect("content").to_owned();
+ let report = parse_rhi_evidence_report(content.clone()).expect("report");
+ assert_eq!(report.outcome, FfiTradeEvidenceOutcome::Indeterminate);
+ assert_eq!(report.trade_generation, "7");
+ assert_eq!(report.observed_at_unix_s, "1800000000");
+
+ let plan = prepare_rhi_evidence_attestation(content, 1_784_347_200).expect("plan");
+ assert_eq!(plan.kind, 3_441);
+ assert_eq!(plan.created_at_unix_s, "1784347200");
+ assert_eq!(
+ plan.expected_event_id,
+ expected["event_id"].as_str().expect("event id")
+ );
+
+ let raw: serde_json::Value =
+ serde_json::from_str(expected["raw_json"].as_str().expect("raw event"))
+ .expect("raw event JSON");
+ let signed = FfiSignedEvidenceEventRecord {
+ id: raw["id"].as_str().expect("id").to_owned(),
+ author_pubkey: raw["pubkey"].as_str().expect("pubkey").to_owned(),
+ created_at_unix_s: raw["created_at"].as_u64().expect("created_at"),
+ kind: u32::try_from(raw["kind"].as_u64().expect("kind")).expect("u32 kind"),
+ tags: serde_json::from_value(raw["tags"].clone()).expect("tags"),
+ content: raw["content"].as_str().expect("content").to_owned(),
+ signature: raw["sig"].as_str().expect("signature").to_owned(),
+ };
+ let attestation = validate_rhi_evidence_attestation(signed).expect("attestation");
+ assert_eq!(attestation.outcome, FfiTradeEvidenceOutcome::Indeterminate);
+ assert_eq!(attestation.trade_generation, "7");
+ }
+
+ #[test]
+ fn evidence_errors_do_not_render_untrusted_content() {
+ let secret = "mobile-private-evidence";
+ let error = parse_rhi_evidence_report(secret.to_owned()).expect_err("malformed report");
+ assert!(!error.to_string().contains(secret));
+ assert!(!format!("{error:?}").contains(secret));
+ }
+
fn png(width: u32, height: u32) -> Vec<u8> {
let mut bytes = b"\x89PNG\r\n\x1a\n\0\0\0\rIHDR".to_vec();
bytes.extend_from_slice(&width.to_be_bytes());
diff --git a/crates/mobile_ffi/tests/uniffi_contract.rs b/crates/mobile_ffi/tests/uniffi_contract.rs
@@ -1,8 +1,9 @@
use radroots_mobile_ffi::{
FfiAddCommandType, FfiAddDraftInput, FfiCancellationPolicy, FfiMediaOperation,
- FfiQueuePolicyRecord, FfiRelaySatisfaction, HostSigningOutcome, HostSigningRequest,
- HostSigningResult, MOBILE_FFI_SCHEMA_VERSION, ProtectedDataAvailability, RadrootsAppError,
- RadrootsHostSigner, RadrootsRuntime, SignerAvailabilityRecord, SignerStatusRecord,
+ FfiQueuePolicyRecord, FfiRelaySatisfaction, FfiTradeEvidenceCoverage, FfiTradeEvidenceOutcome,
+ HostSigningOutcome, HostSigningRequest, HostSigningResult, MOBILE_FFI_SCHEMA_VERSION,
+ ProtectedDataAvailability, RadrootsAppError, RadrootsHostSigner, RadrootsRuntime,
+ SignerAvailabilityRecord, SignerStatusRecord,
};
use secp256k1::{Keypair, Message, Secp256k1, SecretKey};
use std::sync::{Arc, Mutex};
@@ -77,6 +78,29 @@ fn swift_module_names_preserve_the_host_contract() {
}
#[test]
+fn final_evidence_vocabularies_are_exact_at_the_mobile_boundary() {
+ assert_eq!(
+ [
+ FfiTradeEvidenceCoverage::Missing,
+ FfiTradeEvidenceCoverage::Partial,
+ FfiTradeEvidenceCoverage::ScopeSatisfied,
+ FfiTradeEvidenceCoverage::Unsupported,
+ ]
+ .len(),
+ 4
+ );
+ assert_eq!(
+ [
+ FfiTradeEvidenceOutcome::Valid,
+ FfiTradeEvidenceOutcome::Invalid,
+ FfiTradeEvidenceOutcome::Indeterminate,
+ ]
+ .len(),
+ 3
+ );
+}
+
+#[test]
fn media_cancellation_handle_owns_one_stable_opaque_operation_identity() {
let operation = FfiMediaOperation::new().expect("media operation");
let operation_id = operation.operation_id();
diff --git a/crates/sdk/src/trade.rs b/crates/sdk/src/trade.rs
@@ -10,6 +10,88 @@ use radroots_event_codec::authoring::{AuthoredEventPlan, AuthoredPlanError};
use radroots_signing::Actor;
use radroots_trade::{Projection, ReductionInput, WorkflowPlan, reducer::reduce_trade_records};
+pub use radroots_event_codec::decode::rhi::{
+ RadrootsRhiEvidenceAttestationError, RadrootsRhiEvidenceAttestationOutcomeV1,
+ RadrootsRhiEvidenceAttestationSupersessionV1, RadrootsRhiEvidenceAttestationV1,
+};
+pub use radroots_trade::evidence::{
+ RadrootsRhiEvidenceReasonCodeV1, RadrootsRhiEvidenceReportError, RadrootsRhiEvidenceReportV1,
+ RadrootsRhiEvidenceStatementDigestV1, RadrootsRhiEvidenceSupersessionV1,
+ RadrootsTradeEvidenceCoverageError, RadrootsTradeEvidenceCoverageV1,
+ RadrootsTradeEvidenceManifestDigestV1, RadrootsTradeEvidenceManifestError,
+ RadrootsTradeEvidenceManifestObservationV1, RadrootsTradeEvidenceManifestSourceResultV1,
+ RadrootsTradeEvidenceManifestV1, RadrootsTradeEvidenceOutcomeV1,
+ RadrootsTradeEvidencePolicyDigestV1, RadrootsTradeEvidenceProjectionDigestV1,
+ RadrootsTradeEvidenceProvenanceDigestV1, RadrootsTradeEvidenceScopePrerequisitesV1,
+ RadrootsTradeEvidenceSourceCompletionV1, RadrootsTradeEvidenceSourceIdV1,
+ RadrootsTradeEvidenceSourceRequirementV1, RadrootsTradeEvidenceSourceResultDigestV1,
+ RadrootsTradeEvidenceSourceResultV1, RadrootsTradeSignedEventDigestV1,
+ classify_trade_evidence_coverage_v1,
+};
+
+/// Parses one bounded canonical trade-evidence manifest.
+pub fn parse_evidence_manifest(
+ canonical_bytes: &[u8],
+) -> Result<RadrootsTradeEvidenceManifestV1, RadrootsTradeEvidenceManifestError> {
+ RadrootsTradeEvidenceManifestV1::from_canonical_bytes(canonical_bytes)
+}
+
+/// Parses one bounded canonical RHI evidence report.
+pub fn parse_rhi_evidence_report(
+ canonical_content: &[u8],
+) -> Result<RadrootsRhiEvidenceReportV1, RadrootsRhiEvidenceReportError> {
+ RadrootsRhiEvidenceReportV1::from_canonical_content(canonical_content)
+}
+
+/// Builds one immutable typed RHI attestation plan without signing or I/O.
+pub fn prepare_rhi_evidence_attestation(
+ report: &RadrootsRhiEvidenceReportV1,
+ created_at: u64,
+) -> Result<AuthoredEventPlan, AuthoredPlanError> {
+ let attestation = RadrootsRhiEvidenceAttestationV1::from_canonical_content(
+ report.canonical_content().as_bytes(),
+ )
+ .map_err(AuthoredPlanError::Rhi)?;
+ AuthoredEventPlan::from_rhi_evidence_attestation(&attestation, created_at)
+}
+
+/// Verifies NIP-01 identity/signature and then validates the exact RHI event.
+pub fn validate_rhi_evidence_attestation(
+ event: radroots_event::envelope::EventEnvelope,
+) -> Result<RadrootsRhiEvidenceAttestationV1, EvidenceAttestationValidationError> {
+ let verified =
+ radroots_event_codec::verify::id(radroots_event::admission::RawEvent::new(event))
+ .and_then(|event| {
+ radroots_event_codec::verify::signature(
+ event,
+ &radroots_event_codec::verify::Nip01SignatureVerifier,
+ )
+ })
+ .map_err(|_| EvidenceAttestationValidationError::Signature)?;
+ radroots_event_codec::decode::rhi::rhi_evidence_attestation_from_verified_event(&verified)
+ .map_err(|_| EvidenceAttestationValidationError::Contract)
+}
+
+/// Stable, value-free failure for SDK signed-attestation validation.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum EvidenceAttestationValidationError {
+ /// NIP-01 event identity or signature verification failed.
+ Signature,
+ /// The verified event does not satisfy the RHI attestation contract.
+ Contract,
+}
+
+impl fmt::Display for EvidenceAttestationValidationError {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str(match self {
+ Self::Signature => "evidence attestation signature validation failed",
+ Self::Contract => "evidence attestation contract validation failed",
+ })
+ }
+}
+
+impl error::Error for EvidenceAttestationValidationError {}
+
/// Pure inputs for one frozen trade command.
#[derive(Clone, Debug)]
pub struct PrepareRequest {
@@ -391,6 +473,22 @@ mod tests {
.expect("actor")
}
+ fn rhi_attestation_fixture() -> serde_json::Value {
+ let fixture: serde_json::Value = serde_json::from_str(include_str!(
+ "../../../contracts/conformance/vectors/event/authored_operations.v1.json"
+ ))
+ .expect("authored corpus");
+ fixture["vectors"]
+ .as_array()
+ .expect("operations")
+ .iter()
+ .find(|entry| entry["id"] == "typed_rhi_evidence_attestation_017")
+ .expect("RHI operation")
+ .get("expected")
+ .expect("expected")
+ .clone()
+ }
+
fn mutation_id(marker: char) -> MutationId {
MutationId::parse(std::iter::repeat_n(marker, 64).collect::<String>()).expect("mutation id")
}
@@ -666,6 +764,67 @@ mod tests {
assert!(!projection.projection_digest().is_empty());
}
+ #[test]
+ fn evidence_adapters_parse_plan_and_verify_the_authored_corpus_event() {
+ let expected = rhi_attestation_fixture();
+ let content = expected["content"].as_str().expect("content");
+ let report = parse_rhi_evidence_report(content.as_bytes()).expect("report");
+ assert_eq!(
+ report.outcome(),
+ RadrootsTradeEvidenceOutcomeV1::Indeterminate
+ );
+
+ let plan = prepare_rhi_evidence_attestation(&report, 1_784_347_200).expect("plan");
+ assert_eq!(plan.body().kind(), 3_441);
+ assert_eq!(
+ plan.expected_event_id().to_hex(),
+ expected["event_id"].as_str().expect("event id")
+ );
+
+ let raw: serde_json::Value =
+ serde_json::from_str(expected["raw_json"].as_str().expect("raw event"))
+ .expect("raw event JSON");
+ let event = radroots_event::envelope::EventEnvelope::new(
+ radroots_event::envelope::EventEnvelopeParts {
+ id: raw["id"].as_str().expect("id").to_owned(),
+ author: raw["pubkey"].as_str().expect("pubkey").to_owned(),
+ created_at: raw["created_at"].as_u64().expect("created_at"),
+ kind: u32::try_from(raw["kind"].as_u64().expect("kind")).expect("u32 kind"),
+ tags: serde_json::from_value(raw["tags"].clone()).expect("tags"),
+ content: raw["content"].as_str().expect("content").to_owned(),
+ sig: raw["sig"].as_str().expect("signature").to_owned(),
+ },
+ )
+ .expect("event");
+ let attestation = validate_rhi_evidence_attestation(event).expect("attestation");
+ assert_eq!(attestation.trade_generation().get(), 7);
+ }
+
+ #[test]
+ fn evidence_validation_error_is_stable_and_value_free() {
+ let expected = rhi_attestation_fixture();
+ let raw: serde_json::Value =
+ serde_json::from_str(expected["raw_json"].as_str().expect("raw event"))
+ .expect("raw event JSON");
+ let event = radroots_event::envelope::EventEnvelope::new(
+ radroots_event::envelope::EventEnvelopeParts {
+ id: raw["id"].as_str().expect("id").to_owned(),
+ author: raw["pubkey"].as_str().expect("pubkey").to_owned(),
+ created_at: raw["created_at"].as_u64().expect("created_at"),
+ kind: u32::try_from(raw["kind"].as_u64().expect("kind")).expect("u32 kind"),
+ tags: serde_json::from_value(raw["tags"].clone()).expect("tags"),
+ content: "private-tamper".to_owned(),
+ sig: raw["sig"].as_str().expect("signature").to_owned(),
+ },
+ )
+ .expect("event");
+ let error = validate_rhi_evidence_attestation(event).expect_err("signature mismatch");
+ assert_eq!(error, EvidenceAttestationValidationError::Signature);
+ assert!(std::error::Error::source(&error).is_none());
+ assert!(!error.to_string().contains("private-tamper"));
+ assert!(!format!("{error:?}").contains("private-tamper"));
+ }
+
#[cfg(all(feature = "sync", feature = "memory", feature = "local-signing"))]
mod operations {
use std::sync::{
diff --git a/crates/sdk/tests/package_boundary.rs b/crates/sdk/tests/package_boundary.rs
@@ -376,6 +376,34 @@ fn trade_planning_uses_only_the_typed_mutation_authoring_boundary() {
}
#[test]
+fn trade_evidence_surface_is_curated_and_signature_verified() {
+ let production = TRADE
+ .split_once("#[cfg(test)]")
+ .expect("trade tests remain separated")
+ .0;
+ for required in [
+ "pub use radroots_trade::evidence::{",
+ "pub fn parse_evidence_manifest(",
+ "pub fn parse_rhi_evidence_report(",
+ "pub fn prepare_rhi_evidence_attestation(",
+ "pub fn validate_rhi_evidence_attestation(",
+ "Nip01SignatureVerifier",
+ "rhi_evidence_attestation_from_verified_event",
+ ] {
+ assert!(
+ production.contains(required),
+ "missing trade evidence boundary `{required}`"
+ );
+ }
+ for forbidden in ["SecretKey", "sign_event", "tokio::spawn", "publish_event("] {
+ assert!(
+ !production.contains(forbidden),
+ "trade evidence surface gained forbidden authority `{forbidden}`"
+ );
+ }
+}
+
+#[test]
fn farm_operations_preserve_pure_planning_commit_and_privacy_boundaries() {
for required in [
"encode::farm::to_wire_parts",
diff --git a/crates/sdk_ffi/Cargo.toml b/crates/sdk_ffi/Cargo.toml
@@ -19,11 +19,15 @@ name = "radroots-sdk-bindgen"
path = "src/bin/bindgen.rs"
[dependencies]
+hex = { workspace = true }
+radroots_event = { workspace = true, features = ["std"] }
+radroots_event_codec = { workspace = true, features = ["json", "std"] }
radroots_sdk = { workspace = true, features = ["memory"] }
thiserror = { workspace = true }
uniffi = { workspace = true, features = ["cli"] }
[dev-dependencies]
+serde_json = { workspace = true }
tokio = { workspace = true, features = ["macros", "rt"] }
[lints]
diff --git a/crates/sdk_ffi/src/lib.rs b/crates/sdk_ffi/src/lib.rs
@@ -62,6 +62,313 @@ pub mod v1 {
}
}
+ impl Error {
+ fn contract(code: &'static str, message: &'static str) -> Self {
+ Self::Sdk {
+ code: code.to_owned(),
+ message: message.to_owned(),
+ retryable: false,
+ }
+ }
+ }
+
+ /// Final evidence coverage vocabulary.
+ #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
+ pub enum TradeEvidenceCoverage {
+ Missing,
+ Partial,
+ ScopeSatisfied,
+ Unsupported,
+ }
+
+ /// Final evidence outcome vocabulary.
+ #[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
+ pub enum TradeEvidenceOutcome {
+ Valid,
+ Invalid,
+ Indeterminate,
+ }
+
+ /// Bounded projection of one canonical evidence manifest.
+ #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
+ pub struct TradeEvidenceManifest {
+ pub contract_id: String,
+ pub contract_version: u16,
+ pub trade_id: String,
+ pub trade_generation: String,
+ pub observed_at_unix_s: String,
+ pub coverage: TradeEvidenceCoverage,
+ pub evidence_policy_digest: String,
+ pub manifest_digest: String,
+ pub canonical_bytes_hex: String,
+ }
+
+ /// Exact immutable RHI supersession reference.
+ #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
+ pub struct RhiEvidenceSupersession {
+ pub report_id: String,
+ pub event_id: String,
+ }
+
+ /// Bounded projection of one canonical RHI evidence report.
+ #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
+ pub struct RhiEvidenceReport {
+ pub contract_id: String,
+ pub contract_version: u16,
+ pub issuer_pubkey: String,
+ pub trade_id: String,
+ pub claim_mutation_id: String,
+ pub outcome: TradeEvidenceOutcome,
+ pub reason_codes: Vec<String>,
+ pub projection_digest: String,
+ pub evidence_manifest_digest: String,
+ pub evidence_policy_digest: String,
+ pub observed_at_unix_s: String,
+ pub trade_generation: String,
+ pub statement_digest: String,
+ pub supersession: Option<RhiEvidenceSupersession>,
+ pub canonical_content: String,
+ }
+
+ /// One unsigned typed event plan ready for host-owned signing.
+ #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
+ pub struct TypedEvidenceEventPlan {
+ pub contract_id: String,
+ pub kind: u32,
+ pub author_pubkey: String,
+ pub created_at_unix_s: String,
+ pub expected_event_id: String,
+ pub tags: Vec<Vec<String>>,
+ pub content: String,
+ }
+
+ /// Signed NIP-01 event input for verified attestation admission.
+ #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
+ pub struct SignedEvent {
+ pub id: String,
+ pub author_pubkey: String,
+ pub created_at_unix_s: u64,
+ pub kind: u32,
+ pub tags: Vec<Vec<String>>,
+ pub content: String,
+ pub signature: String,
+ }
+
+ /// Verified final RHI attestation projection.
+ #[derive(Clone, Debug, Eq, PartialEq, uniffi::Record)]
+ pub struct RhiEvidenceAttestation {
+ pub issuer_pubkey: String,
+ pub trade_id: String,
+ pub claim_mutation_id: String,
+ pub outcome: TradeEvidenceOutcome,
+ pub observed_at_unix_s: String,
+ pub trade_generation: String,
+ pub statement_digest: String,
+ pub supersession: Option<RhiEvidenceSupersession>,
+ pub canonical_content: String,
+ }
+
+ #[uniffi::export]
+ pub fn parse_trade_evidence_manifest(
+ canonical_bytes: Vec<u8>,
+ ) -> Result<TradeEvidenceManifest, Error> {
+ let manifest =
+ radroots_sdk::trade::parse_evidence_manifest(&canonical_bytes).map_err(|_| {
+ Error::contract("invalid_evidence_manifest", "evidence manifest is invalid")
+ })?;
+ Ok(TradeEvidenceManifest::from(&manifest))
+ }
+
+ #[uniffi::export]
+ pub fn parse_rhi_evidence_report(
+ canonical_content: String,
+ ) -> Result<RhiEvidenceReport, Error> {
+ let report = radroots_sdk::trade::parse_rhi_evidence_report(canonical_content.as_bytes())
+ .map_err(|_| {
+ Error::contract("invalid_evidence_report", "evidence report is invalid")
+ })?;
+ Ok(RhiEvidenceReport::from(&report))
+ }
+
+ #[uniffi::export]
+ pub fn prepare_rhi_evidence_attestation(
+ canonical_content: String,
+ created_at_unix_s: u64,
+ ) -> Result<TypedEvidenceEventPlan, Error> {
+ let report = radroots_sdk::trade::parse_rhi_evidence_report(canonical_content.as_bytes())
+ .map_err(|_| {
+ Error::contract("invalid_evidence_report", "evidence report is invalid")
+ })?;
+ let plan =
+ radroots_sdk::trade::prepare_rhi_evidence_attestation(&report, created_at_unix_s)
+ .map_err(|_| {
+ Error::contract(
+ "invalid_evidence_attestation_plan",
+ "evidence attestation plan is invalid",
+ )
+ })?;
+ Ok(TypedEvidenceEventPlan::from(&plan))
+ }
+
+ #[uniffi::export]
+ pub fn validate_rhi_evidence_attestation(
+ event: SignedEvent,
+ ) -> Result<RhiEvidenceAttestation, Error> {
+ let event = radroots_event::envelope::EventEnvelope::new(
+ radroots_event::envelope::EventEnvelopeParts {
+ id: event.id,
+ author: event.author_pubkey,
+ created_at: event.created_at_unix_s,
+ kind: event.kind,
+ tags: event.tags,
+ content: event.content,
+ sig: event.signature,
+ },
+ )
+ .map_err(|_| Error::contract("invalid_signed_event", "signed event is invalid"))?;
+ let attestation =
+ radroots_sdk::trade::validate_rhi_evidence_attestation(event).map_err(|error| {
+ match error {
+ radroots_sdk::trade::EvidenceAttestationValidationError::Signature => {
+ Error::contract(
+ "invalid_event_signature",
+ "event signature validation failed",
+ )
+ }
+ radroots_sdk::trade::EvidenceAttestationValidationError::Contract => {
+ Error::contract(
+ "invalid_evidence_attestation",
+ "evidence attestation is invalid",
+ )
+ }
+ }
+ })?;
+ Ok(RhiEvidenceAttestation::from(&attestation))
+ }
+
+ impl From<&radroots_sdk::trade::RadrootsTradeEvidenceManifestV1> for TradeEvidenceManifest {
+ fn from(value: &radroots_sdk::trade::RadrootsTradeEvidenceManifestV1) -> Self {
+ Self {
+ contract_id: value.contract_id().to_owned(),
+ contract_version: value.contract_version(),
+ trade_id: value.trade_id().to_string(),
+ trade_generation: value.trade_generation().get().to_string(),
+ observed_at_unix_s: value.observed_at_unix_s().to_string(),
+ coverage: value.coverage().into(),
+ evidence_policy_digest: value.evidence_policy_digest().to_hex(),
+ manifest_digest: value.digest().to_hex(),
+ canonical_bytes_hex: hex::encode(value.canonical_bytes()),
+ }
+ }
+ }
+
+ impl From<radroots_sdk::trade::RadrootsTradeEvidenceCoverageV1> for TradeEvidenceCoverage {
+ fn from(value: radroots_sdk::trade::RadrootsTradeEvidenceCoverageV1) -> Self {
+ match value {
+ radroots_sdk::trade::RadrootsTradeEvidenceCoverageV1::Missing => Self::Missing,
+ radroots_sdk::trade::RadrootsTradeEvidenceCoverageV1::Partial => Self::Partial,
+ radroots_sdk::trade::RadrootsTradeEvidenceCoverageV1::ScopeSatisfied => {
+ Self::ScopeSatisfied
+ }
+ radroots_sdk::trade::RadrootsTradeEvidenceCoverageV1::Unsupported => {
+ Self::Unsupported
+ }
+ }
+ }
+ }
+
+ impl From<radroots_sdk::trade::RadrootsTradeEvidenceOutcomeV1> for TradeEvidenceOutcome {
+ fn from(value: radroots_sdk::trade::RadrootsTradeEvidenceOutcomeV1) -> Self {
+ match value {
+ radroots_sdk::trade::RadrootsTradeEvidenceOutcomeV1::Valid => Self::Valid,
+ radroots_sdk::trade::RadrootsTradeEvidenceOutcomeV1::Invalid => Self::Invalid,
+ radroots_sdk::trade::RadrootsTradeEvidenceOutcomeV1::Indeterminate => {
+ Self::Indeterminate
+ }
+ }
+ }
+ }
+
+ impl From<&radroots_sdk::trade::RadrootsRhiEvidenceReportV1> for RhiEvidenceReport {
+ fn from(value: &radroots_sdk::trade::RadrootsRhiEvidenceReportV1) -> Self {
+ Self {
+ contract_id: value.contract_id().to_owned(),
+ contract_version: value.contract_version(),
+ issuer_pubkey: value.issuer_public_key().to_hex(),
+ trade_id: value.trade_id().to_string(),
+ claim_mutation_id: value.claim_mutation_id().to_string(),
+ outcome: value.outcome().into(),
+ reason_codes: value
+ .reason_codes()
+ .iter()
+ .map(|code| code.as_str().to_owned())
+ .collect(),
+ projection_digest: value.projection_digest().to_hex(),
+ evidence_manifest_digest: value.evidence_manifest_digest().to_hex(),
+ evidence_policy_digest: value.evidence_policy_digest().to_hex(),
+ observed_at_unix_s: value.observed_at_unix_s().to_string(),
+ trade_generation: value.trade_generation().get().to_string(),
+ statement_digest: value.statement_digest().to_hex(),
+ supersession: value.supersession().map(RhiEvidenceSupersession::from),
+ canonical_content: value.canonical_content().to_owned(),
+ }
+ }
+ }
+
+ impl From<radroots_sdk::trade::RadrootsRhiEvidenceSupersessionV1> for RhiEvidenceSupersession {
+ fn from(value: radroots_sdk::trade::RadrootsRhiEvidenceSupersessionV1) -> Self {
+ Self {
+ report_id: value.report_id().to_hex(),
+ event_id: value.event_id().to_hex(),
+ }
+ }
+ }
+
+ impl From<&radroots_event_codec::authoring::AuthoredEventPlan> for TypedEvidenceEventPlan {
+ fn from(value: &radroots_event_codec::authoring::AuthoredEventPlan) -> Self {
+ Self {
+ contract_id: value.body().contract().contract_id().as_str().to_owned(),
+ kind: value.body().kind(),
+ author_pubkey: value.author().to_hex(),
+ created_at_unix_s: value.created_at().to_string(),
+ expected_event_id: value.expected_event_id().to_hex(),
+ tags: value.body().tags().to_vec(),
+ content: value.body().content().to_owned(),
+ }
+ }
+ }
+
+ impl From<&radroots_sdk::trade::RadrootsRhiEvidenceAttestationV1> for RhiEvidenceAttestation {
+ fn from(value: &radroots_sdk::trade::RadrootsRhiEvidenceAttestationV1) -> Self {
+ Self {
+ issuer_pubkey: value.issuer().to_hex(),
+ trade_id: value.trade_id().to_string(),
+ claim_mutation_id: value.claim_mutation_id().to_string(),
+ outcome: match value.outcome() {
+ radroots_sdk::trade::RadrootsRhiEvidenceAttestationOutcomeV1::Valid => {
+ TradeEvidenceOutcome::Valid
+ }
+ radroots_sdk::trade::RadrootsRhiEvidenceAttestationOutcomeV1::Invalid => {
+ TradeEvidenceOutcome::Invalid
+ }
+ radroots_sdk::trade::RadrootsRhiEvidenceAttestationOutcomeV1::Indeterminate => {
+ TradeEvidenceOutcome::Indeterminate
+ }
+ },
+ observed_at_unix_s: value.observed_at_unix_s().to_string(),
+ trade_generation: value.trade_generation().get().to_string(),
+ statement_digest: hex::encode(value.statement_digest()),
+ supersession: value
+ .supersession()
+ .map(|supersession| RhiEvidenceSupersession {
+ report_id: hex::encode(supersession.report_id()),
+ event_id: supersession.event_id().to_hex(),
+ }),
+ canonical_content: value.canonical_content().to_owned(),
+ }
+ }
+ }
+
/// Thread-safe mobile handle delegating lifecycle to [`radroots_sdk::Client`].
#[derive(uniffi::Object)]
pub struct MobileClient {
@@ -132,7 +439,11 @@ pub mod v1 {
#[cfg(test)]
mod tests {
- use super::v1::{CapabilityAvailability, Error, MobileClient};
+ use super::v1::{
+ CapabilityAvailability, Error, MobileClient, SignedEvent, TradeEvidenceOutcome,
+ parse_rhi_evidence_report, prepare_rhi_evidence_attestation,
+ validate_rhi_evidence_attestation,
+ };
fn assert_send_sync<T: Send + Sync>() {}
@@ -170,4 +481,62 @@ mod tests {
assert_eq!(message, "SDK storage capability is not configured");
assert!(!retryable);
}
+
+ #[test]
+ fn final_report_plan_and_signed_event_cross_the_ffi_without_numeric_narrowing() {
+ let fixture: serde_json::Value = serde_json::from_str(include_str!(
+ "../../../contracts/conformance/vectors/event/authored_operations.v1.json"
+ ))
+ .expect("authored corpus");
+ let expected = fixture["vectors"]
+ .as_array()
+ .expect("operations")
+ .iter()
+ .find(|entry| entry["id"] == "typed_rhi_evidence_attestation_017")
+ .expect("RHI operation")
+ .get("expected")
+ .expect("expected");
+ let content = expected["content"].as_str().expect("content").to_owned();
+
+ let report = parse_rhi_evidence_report(content.clone()).expect("report");
+ assert_eq!(report.outcome, TradeEvidenceOutcome::Indeterminate);
+ assert_eq!(report.trade_generation, "7");
+ assert_eq!(report.observed_at_unix_s, "1800000000");
+ assert_eq!(report.canonical_content, content);
+
+ let plan = prepare_rhi_evidence_attestation(content, 1_784_347_200).expect("plan");
+ assert_eq!(plan.kind, 3_441);
+ assert_eq!(plan.created_at_unix_s, "1784347200");
+ assert_eq!(
+ plan.expected_event_id,
+ expected["event_id"].as_str().expect("expected event id")
+ );
+
+ let raw: serde_json::Value =
+ serde_json::from_str(expected["raw_json"].as_str().expect("raw event"))
+ .expect("raw event JSON");
+ let signed = SignedEvent {
+ id: raw["id"].as_str().expect("id").to_owned(),
+ author_pubkey: raw["pubkey"].as_str().expect("pubkey").to_owned(),
+ created_at_unix_s: raw["created_at"].as_u64().expect("created_at"),
+ kind: u32::try_from(raw["kind"].as_u64().expect("kind")).expect("u32 kind"),
+ tags: serde_json::from_value(raw["tags"].clone()).expect("tags"),
+ content: raw["content"].as_str().expect("content").to_owned(),
+ signature: raw["sig"].as_str().expect("signature").to_owned(),
+ };
+ let attestation = validate_rhi_evidence_attestation(signed).expect("signed attestation");
+ assert_eq!(attestation.outcome, TradeEvidenceOutcome::Indeterminate);
+ assert_eq!(attestation.trade_generation, "7");
+ }
+
+ #[test]
+ fn ffi_contract_errors_are_bounded_and_secret_safe() {
+ let secret = "private-report-content";
+ let error = parse_rhi_evidence_report(secret.to_owned()).expect_err("malformed report");
+ let display = error.to_string();
+ let debug = format!("{error:?}");
+ assert!(!display.contains(secret));
+ assert!(!debug.contains(secret));
+ assert!(display.contains("invalid_evidence_report"));
+ }
}
diff --git a/crates/trade_bindings/src/lib.rs b/crates/trade_bindings/src/lib.rs
@@ -23,6 +23,7 @@ pub struct TradeTypeInventoryEntry {
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TradeLargeIntegerPolicy {
JsonNumberSafeCount,
+ DecimalStringU64,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
@@ -46,6 +47,13 @@ pub const TRADE_TYPE_INVENTORY: &[TradeTypeInventoryEntry] = &[
source_root("RadrootsTradeAttestationStateV1"),
source_root("RadrootsTradeConflictStateV1"),
source_root("RadrootsTradeEvidenceStateV1"),
+ source_root("RadrootsTradeEvidenceCoverageV1"),
+ source_root("RadrootsTradeEvidenceOutcomeV1"),
+ source_root("RadrootsTradeEvidenceManifestV1"),
+ source_root("RadrootsRhiEvidenceSupersessionV1"),
+ source_root("RadrootsRhiEvidenceReportV1"),
+ source_root("RadrootsRhiEvidenceAttestationV1"),
+ local_shape("RadrootsTypedEvidenceEventPlanV1"),
source_root("RadrootsTradeFulfillmentStateV1"),
source_root("RadrootsOperationalListingTradeProjection"),
local_shape("RadrootsTradeListingBackofficeOverlay"),
@@ -75,6 +83,13 @@ pub const TRADE_TYPE_INVENTORY: &[TradeTypeInventoryEntry] = &[
];
pub const TRADE_LARGE_INTEGER_POLICIES: &[TradeLargeIntegerPolicyEntry] = &[
+ decimal_string_u64("RadrootsTradeEvidenceManifestV1", "trade_generation"),
+ decimal_string_u64("RadrootsTradeEvidenceManifestV1", "observed_at_unix_s"),
+ decimal_string_u64("RadrootsRhiEvidenceReportV1", "trade_generation"),
+ decimal_string_u64("RadrootsRhiEvidenceReportV1", "observed_at_unix_s"),
+ decimal_string_u64("RadrootsRhiEvidenceAttestationV1", "trade_generation"),
+ decimal_string_u64("RadrootsRhiEvidenceAttestationV1", "observed_at_unix_s"),
+ decimal_string_u64("RadrootsTypedEvidenceEventPlanV1", "created_at_unix_s"),
json_number_safe_count("RadrootsTradeFacetCount", "count"),
json_number_safe_count(
"RadrootsTradeListingBackofficeView",
@@ -127,6 +142,18 @@ const fn json_number_safe_count(
}
}
+#[cfg_attr(coverage_nightly, coverage(off))]
+const fn decimal_string_u64(
+ type_name: &'static str,
+ field_name: &'static str,
+) -> TradeLargeIntegerPolicyEntry {
+ TradeLargeIntegerPolicyEntry {
+ type_name,
+ field_name,
+ policy: TradeLargeIntegerPolicy::DecimalStringU64,
+ }
+}
+
/// Validates that the checked-in trade binding inventory is internally coherent.
#[must_use]
pub fn inventory_is_valid() -> bool {
@@ -213,6 +240,13 @@ mod tests {
"RadrootsTradeAttestationStateV1",
"RadrootsTradeConflictStateV1",
"RadrootsTradeEvidenceStateV1",
+ "RadrootsTradeEvidenceCoverageV1",
+ "RadrootsTradeEvidenceOutcomeV1",
+ "RadrootsTradeEvidenceManifestV1",
+ "RadrootsRhiEvidenceSupersessionV1",
+ "RadrootsRhiEvidenceReportV1",
+ "RadrootsRhiEvidenceAttestationV1",
+ "RadrootsTypedEvidenceEventPlanV1",
"RadrootsTradeFulfillmentStateV1",
"RadrootsOperationalListingTradeProjection",
"RadrootsTradeListingBackofficeOverlay",
@@ -277,6 +311,12 @@ mod tests {
"RadrootsTradeAttestationStateV1",
"RadrootsTradeConflictStateV1",
"RadrootsTradeEvidenceStateV1",
+ "RadrootsTradeEvidenceCoverageV1",
+ "RadrootsTradeEvidenceOutcomeV1",
+ "RadrootsTradeEvidenceManifestV1",
+ "RadrootsRhiEvidenceSupersessionV1",
+ "RadrootsRhiEvidenceReportV1",
+ "RadrootsRhiEvidenceAttestationV1",
"RadrootsTradeFulfillmentStateV1",
"RadrootsOperationalListingTradeProjection",
"RadrootsOperationalListingSubtotal",
@@ -309,6 +349,41 @@ mod tests {
actual,
[
(
+ "RadrootsTradeEvidenceManifestV1",
+ "trade_generation",
+ super::TradeLargeIntegerPolicy::DecimalStringU64
+ ),
+ (
+ "RadrootsTradeEvidenceManifestV1",
+ "observed_at_unix_s",
+ super::TradeLargeIntegerPolicy::DecimalStringU64
+ ),
+ (
+ "RadrootsRhiEvidenceReportV1",
+ "trade_generation",
+ super::TradeLargeIntegerPolicy::DecimalStringU64
+ ),
+ (
+ "RadrootsRhiEvidenceReportV1",
+ "observed_at_unix_s",
+ super::TradeLargeIntegerPolicy::DecimalStringU64
+ ),
+ (
+ "RadrootsRhiEvidenceAttestationV1",
+ "trade_generation",
+ super::TradeLargeIntegerPolicy::DecimalStringU64
+ ),
+ (
+ "RadrootsRhiEvidenceAttestationV1",
+ "observed_at_unix_s",
+ super::TradeLargeIntegerPolicy::DecimalStringU64
+ ),
+ (
+ "RadrootsTypedEvidenceEventPlanV1",
+ "created_at_unix_s",
+ super::TradeLargeIntegerPolicy::DecimalStringU64
+ ),
+ (
"RadrootsTradeFacetCount",
"count",
super::TradeLargeIntegerPolicy::JsonNumberSafeCount
diff --git a/crates/trade_bindings/tests/fixtures/generated_types.ts b/crates/trade_bindings/tests/fixtures/generated_types.ts
@@ -23,6 +23,20 @@ export type RadrootsTradeConflictStateV1 = "none" | "concurrent_candidates" | "d
export type RadrootsTradeEvidenceStateV1 = "complete" | "missing" | "query_partial" | "unsupported_version";
+export type RadrootsTradeEvidenceCoverageV1 = "missing" | "partial" | "scope_satisfied" | "unsupported";
+
+export type RadrootsTradeEvidenceOutcomeV1 = "valid" | "invalid" | "indeterminate";
+
+export type RadrootsTradeEvidenceManifestV1 = { contract_id: "radroots.trade.evidence-manifest.v1", contract_version: 1, trade_id: string, trade_generation: string, observed_at_unix_s: string, coverage: RadrootsTradeEvidenceCoverageV1, evidence_policy_digest: string, manifest_digest: string, canonical_bytes_hex: string, };
+
+export type RadrootsRhiEvidenceSupersessionV1 = { report_id: string, event_id: string, };
+
+export type RadrootsRhiEvidenceReportV1 = { contract_id: "radroots.rhi.evidence_attestation.v1", contract_version: 1, issuer_pubkey: string, trade_id: string, claim_mutation_id: string, outcome: RadrootsTradeEvidenceOutcomeV1, reason_codes: Array<string>, projection_digest: string, evidence_manifest_digest: string, evidence_policy_digest: string, observed_at_unix_s: string, trade_generation: string, statement_digest: string, supersession?: RadrootsRhiEvidenceSupersessionV1 | null, canonical_content: string, };
+
+export type RadrootsRhiEvidenceAttestationV1 = { issuer_pubkey: string, trade_id: string, claim_mutation_id: string, outcome: RadrootsTradeEvidenceOutcomeV1, observed_at_unix_s: string, trade_generation: string, statement_digest: string, supersession?: RadrootsRhiEvidenceSupersessionV1 | null, canonical_content: string, };
+
+export type RadrootsTypedEvidenceEventPlanV1 = { contract_id: string, kind: number, author_pubkey: string, created_at_unix_s: string, expected_event_id: string, tags: Array<Array<string>>, content: string, };
+
export type RadrootsTradeFacetCount = { key: string, count: number, };
export type RadrootsTradeFulfillmentStateV1 = "not_started";
diff --git a/tools/xtask/src/sdk_generation/dto_roots.rs b/tools/xtask/src/sdk_generation/dto_roots.rs
@@ -493,6 +493,13 @@ mod tests {
"RadrootsTradeAttestationStateV1",
"RadrootsTradeConflictStateV1",
"RadrootsTradeEvidenceStateV1",
+ "RadrootsTradeEvidenceCoverageV1",
+ "RadrootsTradeEvidenceOutcomeV1",
+ "RadrootsTradeEvidenceManifestV1",
+ "RadrootsRhiEvidenceSupersessionV1",
+ "RadrootsRhiEvidenceReportV1",
+ "RadrootsRhiEvidenceAttestationV1",
+ "RadrootsTypedEvidenceEventPlanV1",
"RadrootsTradeFacetCount",
"RadrootsTradeFulfillmentStateV1",
"RadrootsTradeListingBackofficeOverlay",
diff --git a/tools/xtask/src/sdk_generation/output.rs b/tools/xtask/src/sdk_generation/output.rs
@@ -67,7 +67,7 @@ const EVENT_BINDINGS_TYPES_SHA256: &str =
const TRADE_BINDINGS_TYPES_TS: &str =
include_str!("../../../../crates/trade_bindings/tests/fixtures/generated_types.ts");
const TRADE_BINDINGS_TYPES_SHA256: &str =
- "49560c572206095bd7de5cfd377c234a7f136e4affc13d53516ae2c4b8998e87";
+ "66e6a378231e5341a59cf331b23b3164b27a43fa00411e889d634e4d9d19b53d";
const EVENT_NATIVE_SOURCES: &[&str] = &["radroots_event", "radroots_core"];
const TRADE_NATIVE_SOURCES: &[&str] = &["radroots_trade", "radroots_event", "radroots_core"];
diff --git a/tools/xtask/src/sdk_generation/wasm_declarations.rs b/tools/xtask/src/sdk_generation/wasm_declarations.rs
@@ -26,6 +26,7 @@ struct WasmDeclarationInventory {
struct WasmPublicFunction {
name: &'static str,
parameter: Option<&'static str>,
+ bigint_parameter: Option<&'static str>,
return_kind: WasmReturnKind,
low_level: LowLevelSignature,
}
@@ -39,7 +40,8 @@ enum WasmReturnKind {
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct LowLevelSignature {
- parameters: usize,
+ number_parameters: usize,
+ bigint_parameters: usize,
returns: usize,
}
@@ -62,9 +64,27 @@ macro_rules! public_function {
WasmPublicFunction {
name: $name,
parameter: $parameter,
+ bigint_parameter: None,
return_kind: WasmReturnKind::$return_kind,
low_level: LowLevelSignature {
- parameters: $low_parameters,
+ number_parameters: $low_parameters,
+ bigint_parameters: 0,
+ returns: $low_returns,
+ },
+ }
+ };
+}
+
+macro_rules! public_function_with_bigint {
+ ($name:literal, $parameter:literal, $bigint_parameter:literal, $return_kind:ident, $low_number_parameters:literal, $low_bigint_parameters:literal, $low_returns:literal) => {
+ WasmPublicFunction {
+ name: $name,
+ parameter: Some($parameter),
+ bigint_parameter: Some($bigint_parameter),
+ return_kind: WasmReturnKind::$return_kind,
+ low_level: LowLevelSignature {
+ number_parameters: $low_number_parameters,
+ bigint_parameters: $low_bigint_parameters,
returns: $low_returns,
},
}
@@ -94,7 +114,8 @@ macro_rules! void_function_export {
WasmSupportExport {
name: $name,
export_type: WasmSupportExportType::Function(LowLevelSignature {
- parameters: $parameters,
+ number_parameters: $parameters,
+ bigint_parameters: 0,
returns: 0,
}),
}
@@ -161,7 +182,7 @@ fn primary_module(spec: WasmPackageSpec, inventory: WasmDeclarationInventory) ->
for function in inventory.public_functions {
module.push_declaration(TypeScriptDeclaration::function(
function.name,
- public_parameters(function.parameter),
+ public_parameters(function.parameter, function.bigint_parameter),
public_return_type(function.return_kind),
));
}
@@ -243,10 +264,20 @@ fn render_declaration_module(module: TypeScriptModule) -> String {
rendered
}
-fn public_parameters(parameter: Option<&'static str>) -> Vec<TypeScriptParameter> {
- parameter
+fn public_parameters(
+ parameter: Option<&'static str>,
+ bigint_parameter: Option<&'static str>,
+) -> Vec<TypeScriptParameter> {
+ let mut parameters = parameter
.map(|name| vec![TypeScriptParameter::new(name, TypeScriptType::String)])
- .unwrap_or_default()
+ .unwrap_or_default();
+ if let Some(name) = bigint_parameter {
+ parameters.push(TypeScriptParameter::new(
+ name,
+ TypeScriptType::named("bigint"),
+ ));
+ }
+ parameters
}
fn public_return_type(kind: WasmReturnKind) -> TypeScriptType {
@@ -320,10 +351,11 @@ fn low_level_number_function_type(parameters: usize) -> TypeScriptType {
}
fn low_level_function_type(signature: LowLevelSignature) -> TypeScriptType {
- TypeScriptType::function(
- number_parameters(signature.parameters),
- low_level_return_type(signature.returns),
- )
+ let mut parameters = number_parameters(signature.number_parameters);
+ parameters.extend((0..signature.bigint_parameters).map(|index| {
+ TypeScriptParameter::new(format!("i64_arg{index}"), TypeScriptType::named("bigint"))
+ }));
+ TypeScriptType::function(parameters, low_level_return_type(signature.returns))
}
fn number_parameters(count: usize) -> Vec<TypeScriptParameter> {
@@ -433,6 +465,36 @@ const EVENT_CODEC_FUNCTIONS: &[WasmPublicFunction] = &[
public_function!("repost_tags", Some("repost_json"), String, 2, 4),
public_function!("seal_tags", Some("seal_json"), String, 2, 4),
public_function!(
+ "trade_evidence_manifest_parse_json",
+ Some("canonical_bytes_hex"),
+ String,
+ 2,
+ 4
+ ),
+ public_function!(
+ "rhi_evidence_report_parse_json",
+ Some("canonical_content"),
+ String,
+ 2,
+ 4
+ ),
+ public_function_with_bigint!(
+ "rhi_evidence_attestation_build_draft",
+ "canonical_content",
+ "created_at_unix_s",
+ String,
+ 2,
+ 1,
+ 4
+ ),
+ public_function!(
+ "rhi_evidence_attestation_validate_signed_json",
+ Some("event_json"),
+ String,
+ 2,
+ 4
+ ),
+ public_function!(
"social_ask_build_authored_draft",
Some("input_json"),
String,
@@ -1081,6 +1143,25 @@ mod tests {
.contains(&format!("export declare const {authored}"))
);
}
+ for evidence in [
+ "trade_evidence_manifest_parse_json",
+ "rhi_evidence_report_parse_json",
+ "rhi_evidence_attestation_build_draft",
+ "rhi_evidence_attestation_validate_signed_json",
+ ] {
+ assert!(
+ events[0]
+ .contents
+ .contains(&format!("export function {evidence}"))
+ );
+ assert!(
+ events[1]
+ .contents
+ .contains(&format!("export declare const {evidence}"))
+ );
+ }
+ assert!(events[0].contents.contains("created_at_unix_s: bigint"));
+ assert!(events[1].contents.contains("i64_arg0: bigint"));
for retired in [
"calendar_date_event_tags",
"calendar_event_rsvp_tags",