sdk

Radroots SDK and bindings
git clone https://radroots.dev/git/sdk.git
Log | Files | Refs | README

commit 9e1462ae7e9acd785ca09acf0cc81049a68d262d
parent 4638baeffcb0be18c0ee32b799d88422f6edb7d8
Author: triesap <tyson@radroots.org>
Date:   Sun,  5 Jul 2026 06:55:18 +0000

wasm: add knowledge event exports

Diffstat:
MCargo.lock | 1+
Mcrates/events_bindings/src/model.rs | 20++++++++++++++++++++
Mcrates/events_codec_wasm/Cargo.toml | 5+++++
Mcrates/events_codec_wasm/src/lib.rs | 489++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mpackages/events-bindings/src/generated/kinds.ts | 22++++++++++++++++++++++
Mpackages/events-bindings/src/generated/types.ts | 46++++++++++++++++++++++++++++++++++++++++++++++
Mtools/xtask/src/dto_roots.rs | 23+++++++++++++++++++++++
Mtools/xtask/src/wasm_declarations.rs | 34++++++++++++++++++++++++++++++++++
8 files changed, 639 insertions(+), 1 deletion(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -1819,6 +1819,7 @@ dependencies = [ name = "radroots_events_codec_wasm" version = "0.1.0-alpha.2" dependencies = [ + "nostr", "radroots_core", "radroots_events", "radroots_events_codec", diff --git a/crates/events_bindings/src/model.rs b/crates/events_bindings/src/model.rs @@ -45,6 +45,9 @@ const EVENT_KIND_EXPORTS: &[(&str, u32)] = &[ ("KIND_GIFT_WRAP", kinds::KIND_GIFT_WRAP), ("KIND_COMMENT", kinds::KIND_COMMENT), ("KIND_GEOCHAT", kinds::KIND_GEOCHAT), + ("KIND_WIKI_MERGE_REQUEST", kinds::KIND_WIKI_MERGE_REQUEST), + ("KIND_WIKI_ARTICLE", kinds::KIND_WIKI_ARTICLE), + ("KIND_WIKI_REDIRECT", kinds::KIND_WIKI_REDIRECT), ("KIND_LIST_MUTE", kinds::KIND_LIST_MUTE), ("KIND_LIST_PINNED_NOTES", kinds::KIND_LIST_PINNED_NOTES), ( @@ -164,6 +167,23 @@ const EVENT_KIND_EXPORTS: &[(&str, u32)] = &[ "KIND_TRADE_VALIDATION_RECEIPT", kinds::KIND_TRADE_VALIDATION_RECEIPT, ), + ("KIND_KNOWLEDGE_CLAIM", kinds::KIND_KNOWLEDGE_CLAIM), + ("KIND_KNOWLEDGE_RELATION", kinds::KIND_KNOWLEDGE_RELATION), + ("KIND_KNOWLEDGE_REVIEW", kinds::KIND_KNOWLEDGE_REVIEW), + ( + "KIND_KNOWLEDGE_FIELD_REPORT", + kinds::KIND_KNOWLEDGE_FIELD_REPORT, + ), + ( + "KIND_KNOWLEDGE_CHANGE_PROPOSAL", + kinds::KIND_KNOWLEDGE_CHANGE_PROPOSAL, + ), + ( + "KIND_CONTRIBUTION_ATTESTATION", + kinds::KIND_CONTRIBUTION_ATTESTATION, + ), + ("KIND_KNOWLEDGE_SOURCE", kinds::KIND_KNOWLEDGE_SOURCE), + ("KIND_EVIDENCE_BOUNTY", kinds::KIND_EVIDENCE_BOUNTY), ("KIND_JOB_REQUEST_MIN", kinds::KIND_JOB_REQUEST_MIN), ("KIND_JOB_REQUEST_MAX", kinds::KIND_JOB_REQUEST_MAX), ("KIND_JOB_RESULT_MIN", kinds::KIND_JOB_RESULT_MIN), diff --git a/crates/events_codec_wasm/Cargo.toml b/crates/events_codec_wasm/Cargo.toml @@ -18,16 +18,21 @@ crate-type = ["cdylib", "rlib"] radroots_events = { workspace = true, default-features = false, features = [ "std", "serde", + "knowledge-nip54", ] } radroots_events_codec = { workspace = true, default-features = false, features = [ "std", "serde_json", + "nostr", + "knowledge-nip54", + "contract-manifest", ] } serde = { workspace = true } serde_json = { workspace = true } wasm-bindgen = { workspace = true } [dev-dependencies] +nostr = { workspace = true } radroots_core = { workspace = true, default-features = false, features = [ "std", ] } diff --git a/crates/events_codec_wasm/src/lib.rs b/crates/events_codec_wasm/src/lib.rs @@ -25,6 +25,11 @@ use radroots_events::http_auth::RadrootsHttpAuth; use radroots_events::job_feedback::RadrootsJobFeedback; use radroots_events::job_request::RadrootsJobRequest; use radroots_events::job_result::RadrootsJobResult; +use radroots_events::knowledge::{ + RadrootsKnowledgeClaim, RadrootsKnowledgeFieldReport, RadrootsKnowledgeRelation, + RadrootsKnowledgeReview, RadrootsKnowledgeSource, RadrootsWikiArticle, + RadrootsWikiMergeRequest, RadrootsWikiRedirect, +}; use radroots_events::list::RadrootsList; use radroots_events::list_set::RadrootsListSet; use radroots_events::listing::RadrootsListing; @@ -63,6 +68,11 @@ use radroots_events_codec::http_auth::encode::http_auth_build_tags; use radroots_events_codec::job::feedback::encode::job_feedback_build_tags; use radroots_events_codec::job::request::encode::job_request_build_tags; use radroots_events_codec::job::result::encode::job_result_build_tags; +use radroots_events_codec::knowledge::{ + knowledge_claim_build_tags, knowledge_field_report_build_tags, knowledge_relation_build_tags, + knowledge_review_build_tags, knowledge_source_build_tags, wiki_article_build_tags, + wiki_merge_request_build_tags, wiki_redirect_build_tags, +}; use radroots_events_codec::list::encode::list_build_tags; use radroots_events_codec::list_set::encode::list_set_build_tags; use radroots_events_codec::listing::tags::{ @@ -77,6 +87,8 @@ use radroots_events_codec::relay_auth::encode::relay_auth_build_tags; use radroots_events_codec::report::encode::report_build_tags; use radroots_events_codec::repost::encode::{generic_repost_build_tags, repost_build_tags}; use radroots_events_codec::seal::encode::seal_build_tags; +use radroots_events_codec::verification::{RadrootsDecodeError, RadrootsDecodedEvent}; +use serde::Serialize; use serde::de::DeserializeOwned; #[cfg(target_arch = "wasm32")] use wasm_bindgen::JsValue; @@ -100,6 +112,14 @@ fn err_js<E: ToString>(err: E) -> RadrootsJsValue { } } +fn error_json(code: &str, inner_code: Option<&str>) -> RadrootsJsValue { + let value = serde_json::json!({ + "code": code, + "inner_code": inner_code, + }); + err_js(value) +} + fn normalized_payload(input: &str) -> &str { if input.is_empty() { "{}" } else { input } } @@ -112,6 +132,10 @@ fn tags_to_json(tags: Vec<Vec<String>>) -> Result<String, RadrootsJsValue> { serde_json::to_string(&tags).map_err(err_js) } +fn parse_event_json(input: &str) -> Result<radroots_events::RadrootsNostrEvent, RadrootsJsValue> { + serde_json::from_str(input).map_err(|_| error_json("invalid_json", Some("event_json"))) +} + fn build_tags_json<T, E, F>(input: &str, build: F) -> Result<String, RadrootsJsValue> where T: DeserializeOwned, @@ -133,6 +157,94 @@ where tags_to_json(tags) } +#[derive(Serialize)] +struct DecodedEventJson<'a, T> +where + T: Serialize, +{ + event_type: &'static str, + contract_id: &'static str, + event: &'a radroots_events::RadrootsNostrEvent, + payload: &'a T, +} + +fn decoded_event_to_json(decoded: RadrootsDecodedEvent) -> Result<String, RadrootsJsValue> { + match decoded { + RadrootsDecodedEvent::WikiArticle(parsed) => { + decoded_payload_to_json("wiki_article", "radroots.wiki.article.v1", &parsed) + } + RadrootsDecodedEvent::WikiRedirect(parsed) => { + decoded_payload_to_json("wiki_redirect", "radroots.wiki.redirect.v1", &parsed) + } + RadrootsDecodedEvent::WikiMergeRequest(parsed) => decoded_payload_to_json( + "wiki_merge_request", + "radroots.wiki.merge_request.v1", + &parsed, + ), + RadrootsDecodedEvent::KnowledgeSource(parsed) => { + decoded_payload_to_json("knowledge_source", "radroots.knowledge.source.v1", &parsed) + } + RadrootsDecodedEvent::KnowledgeClaim(parsed) => { + decoded_payload_to_json("knowledge_claim", "radroots.knowledge.claim.v1", &parsed) + } + RadrootsDecodedEvent::KnowledgeRelation(parsed) => decoded_payload_to_json( + "knowledge_relation", + "radroots.knowledge.relation.v1", + &parsed, + ), + RadrootsDecodedEvent::KnowledgeReview(parsed) => { + decoded_payload_to_json("knowledge_review", "radroots.knowledge.review.v1", &parsed) + } + RadrootsDecodedEvent::KnowledgeFieldReport(parsed) => decoded_payload_to_json( + "knowledge_field_report", + "radroots.knowledge.field_report.v1", + &parsed, + ), + RadrootsDecodedEvent::EvidenceBounty(parsed) => decoded_payload_to_json( + "evidence_bounty", + "radroots.knowledge.evidence_bounty.v1", + &parsed, + ), + RadrootsDecodedEvent::KnowledgeChangeProposal(parsed) => decoded_payload_to_json( + "knowledge_change_proposal", + "radroots.knowledge.change_proposal.v1", + &parsed, + ), + RadrootsDecodedEvent::ContributionAttestation(parsed) => decoded_payload_to_json( + "contribution_attestation", + "radroots.knowledge.contribution_attestation.v1", + &parsed, + ), + } +} + +fn decoded_payload_to_json<T>( + event_type: &'static str, + contract_id: &'static str, + parsed: &radroots_events_codec::parsed::RadrootsParsedEvent<T>, +) -> Result<String, RadrootsJsValue> +where + T: Serialize, +{ + serde_json::to_string(&DecodedEventJson { + event_type, + contract_id, + event: &parsed.event, + payload: &parsed.data.data, + }) + .map_err(err_js) +} + +fn decode_error_json(error: RadrootsDecodeError) -> RadrootsJsValue { + let inner = match &error { + RadrootsDecodeError::Nip01Verification(error) => Some(error.code()), + RadrootsDecodeError::ContractValidation(error) => Some(error.code()), + RadrootsDecodeError::EventParse(error) => Some(error.code()), + RadrootsDecodeError::UnsupportedContract { .. } => None, + }; + error_json(error.code(), inner) +} + #[derive(serde::Deserialize)] struct FarmCrdtTagsInput { change: RadrootsFarmCrdtChange, @@ -164,6 +276,62 @@ pub fn article_tags(article_json: &str) -> Result<String, RadrootsJsValue> { build_tags_json::<RadrootsArticle, _, _>(article_json, article_build_tags) } +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = wiki_article_tags))] +pub fn wiki_article_tags(article_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<RadrootsWikiArticle, _, _>(article_json, wiki_article_build_tags) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = wiki_redirect_tags))] +pub fn wiki_redirect_tags(redirect_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<RadrootsWikiRedirect, _, _>(redirect_json, wiki_redirect_build_tags) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = wiki_merge_request_tags))] +pub fn wiki_merge_request_tags(request_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<RadrootsWikiMergeRequest, _, _>(request_json, wiki_merge_request_build_tags) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = knowledge_source_tags))] +pub fn knowledge_source_tags(source_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<RadrootsKnowledgeSource, _, _>(source_json, knowledge_source_build_tags) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = knowledge_claim_tags))] +pub fn knowledge_claim_tags(claim_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<RadrootsKnowledgeClaim, _, _>(claim_json, knowledge_claim_build_tags) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = knowledge_relation_tags))] +pub fn knowledge_relation_tags(relation_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<RadrootsKnowledgeRelation, _, _>(relation_json, knowledge_relation_build_tags) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = knowledge_review_tags))] +pub fn knowledge_review_tags(review_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<RadrootsKnowledgeReview, _, _>(review_json, knowledge_review_build_tags) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = knowledge_field_report_tags))] +pub fn knowledge_field_report_tags(report_json: &str) -> Result<String, RadrootsJsValue> { + build_tags_json::<RadrootsKnowledgeFieldReport, _, _>( + report_json, + knowledge_field_report_build_tags, + ) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = verify_and_decode_event_json))] +pub fn verify_and_decode_event_json(event_json: &str) -> Result<String, RadrootsJsValue> { + let event = parse_event_json(event_json)?; + let decoded = radroots_events_codec::verify_and_decode_radroots_event(event) + .map_err(decode_error_json)?; + decoded_event_to_json(decoded) +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = contract_manifest_json))] +pub fn contract_manifest_json() -> Result<String, RadrootsJsValue> { + radroots_events_codec::contract_manifest_json().map_err(err_js) +} + #[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = file_metadata_tags))] pub fn file_metadata_tags(metadata_json: &str) -> Result<String, RadrootsJsValue> { build_tags_json::<RadrootsFileMetadata, _, _>(metadata_json, file_metadata_build_tags) @@ -417,7 +585,19 @@ mod tests { use radroots_events::http_auth::RadrootsHttpAuth; use radroots_events::job::JobInputType; use radroots_events::job_request::{RadrootsJobInput, RadrootsJobParam}; - use radroots_events::kinds::KIND_FARM_FILE_METADATA; + use radroots_events::kinds::{ + KIND_FARM_FILE_METADATA, KIND_FILE_METADATA, KIND_KNOWLEDGE_CLAIM, KIND_KNOWLEDGE_SOURCE, + KIND_WIKI_ARTICLE, + }; + use radroots_events::knowledge::{ + RADROOTS_KNOWLEDGE_CLAIM_SCHEMA, RADROOTS_KNOWLEDGE_FIELD_REPORT_SCHEMA, + RADROOTS_KNOWLEDGE_RELATION_SCHEMA, RADROOTS_KNOWLEDGE_REVIEW_SCHEMA, + RADROOTS_KNOWLEDGE_SCHEMA_VERSION, RADROOTS_KNOWLEDGE_SOURCE_SCHEMA, + RadrootsAddressableRef, RadrootsKnowledgeCitationSpan, RadrootsKnowledgeFieldContext, + RadrootsKnowledgeLocation, RadrootsKnowledgeLocationPrecision, RadrootsKnowledgeNodeRef, + RadrootsKnowledgeObservation, RadrootsKnowledgeObservationValue, + RadrootsKnowledgeReviewScope, RadrootsKnowledgeReviewScore, RadrootsKnowledgeReviewTarget, + }; use radroots_events::listing::{RadrootsListingBin, RadrootsListingProduct}; use radroots_events::relay_auth::RadrootsRelayAuth; use radroots_events::social::{ @@ -512,6 +692,221 @@ mod tests { } } + fn knowledge_event_ref(seed: char, kind: u32) -> radroots_events::RadrootsNostrEventRef { + radroots_events::RadrootsNostrEventRef { + id: synthetic_event_id(seed), + author: synthetic_pubkey('a'), + kind, + d_tag: None, + relays: Some(vec!["wss://relay.example.test".to_string()]), + } + } + + fn knowledge_address_ref() -> RadrootsAddressableRef { + RadrootsAddressableRef { + kind: KIND_WIKI_ARTICLE, + pubkey: synthetic_pubkey('a'), + d_tag: "soil-health".to_string(), + relays: vec!["wss://relay.example.test".to_string()], + } + } + + fn sample_wiki_article() -> RadrootsWikiArticle { + RadrootsWikiArticle { + d_tag: "soil-health".to_string(), + title: "Soil health".to_string(), + content_djot: "# Soil health".to_string(), + summary: Some("Living soil basics".to_string()), + topics: vec!["soil".to_string()], + references: vec![knowledge_event_ref('1', KIND_KNOWLEDGE_SOURCE)], + forked_from: Vec::new(), + deferred_to: None, + } + } + + fn sample_wiki_redirect() -> RadrootsWikiRedirect { + RadrootsWikiRedirect { + d_tag: "soil".to_string(), + target: radroots_events::RadrootsNostrEventRef { + id: synthetic_event_id('b'), + author: synthetic_pubkey('a'), + kind: KIND_WIKI_ARTICLE, + d_tag: Some("soil-health".to_string()), + relays: Some(vec!["wss://relay.example.test".to_string()]), + }, + } + } + + fn sample_wiki_merge_request() -> RadrootsWikiMergeRequest { + RadrootsWikiMergeRequest { + target_article: knowledge_address_ref(), + destination_pubkey: synthetic_pubkey('a'), + base_version_event_id: Some(synthetic_event_id('e')), + source_version_event_id: synthetic_event_id('f'), + explanation: Some("Merge synthetic soil article updates".to_string()), + } + } + + fn sample_knowledge_source() -> RadrootsKnowledgeSource { + RadrootsKnowledgeSource { + schema: RADROOTS_KNOWLEDGE_SOURCE_SCHEMA.to_string(), + schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION, + d_tag: "soil-source".to_string(), + title: "Soil Source".to_string(), + source_type: "book".to_string(), + authors: vec!["A. Example".to_string()], + publisher: Some("Radroots Synthetic Press".to_string()), + publication_year: Some(2026), + edition: None, + canonical_url: Some("https://source.example.test/soil-source".to_string()), + artifact_refs: vec![knowledge_event_ref('3', KIND_FILE_METADATA)], + author_asserted_rights: None, + topics: vec!["soil".to_string()], + summary: Some("Synthetic source for wasm coverage".to_string()), + } + } + + fn sample_knowledge_claim() -> RadrootsKnowledgeClaim { + RadrootsKnowledgeClaim { + schema: RADROOTS_KNOWLEDGE_CLAIM_SCHEMA.to_string(), + schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION, + claim_type: "practice_effect".to_string(), + text: "Cover crops improve soil structure.".to_string(), + citation_spans: vec![RadrootsKnowledgeCitationSpan { + source_ref: knowledge_event_ref('4', KIND_KNOWLEDGE_SOURCE), + artifact_ref: None, + page_start: Some(12), + page_end: Some(13), + section_path: vec!["chapter-1".to_string()], + quote_hash: Some(synthetic_event_id('5')), + chunk_id: Some("chunk-1".to_string()), + }], + topics: vec!["cover-crops".to_string()], + applies_to: vec!["local-food".to_string()], + author_asserted_confidence: Some("medium".to_string()), + supersedes: Vec::new(), + } + } + + fn sample_knowledge_node_ref(label: &str) -> RadrootsKnowledgeNodeRef { + RadrootsKnowledgeNodeRef { + node_type: "event".to_string(), + event_ref: Some(knowledge_event_ref('6', KIND_KNOWLEDGE_CLAIM)), + address_ref: None, + external_id: None, + label: Some(label.to_string()), + } + } + + fn sample_knowledge_relation() -> RadrootsKnowledgeRelation { + RadrootsKnowledgeRelation { + schema: RADROOTS_KNOWLEDGE_RELATION_SCHEMA.to_string(), + schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION, + subject: sample_knowledge_node_ref("cover crops"), + predicate: "supports".to_string(), + object: sample_knowledge_node_ref("soil structure"), + support_refs: vec![knowledge_event_ref('7', KIND_KNOWLEDGE_CLAIM)], + author_asserted_confidence: Some("medium".to_string()), + supersedes: Vec::new(), + } + } + + fn sample_knowledge_review() -> RadrootsKnowledgeReview { + RadrootsKnowledgeReview { + schema: RADROOTS_KNOWLEDGE_REVIEW_SCHEMA.to_string(), + schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION, + target: RadrootsKnowledgeReviewTarget { + event_id: synthetic_event_id('8'), + author_pubkey: synthetic_pubkey('a'), + kind: KIND_KNOWLEDGE_CLAIM, + address: None, + relays: vec!["wss://relay.example.test".to_string()], + review_scope: RadrootsKnowledgeReviewScope::SpecificVersion, + }, + reviewer_role: "peer".to_string(), + verdict: "needs_more_evidence".to_string(), + scores: vec![RadrootsKnowledgeReviewScore { + dimension: "evidence".to_string(), + value: "partial".to_string(), + note: None, + }], + notes: Some("Synthetic review".to_string()), + evidence_refs: vec![knowledge_event_ref('9', KIND_KNOWLEDGE_SOURCE)], + } + } + + fn sample_knowledge_field_report() -> RadrootsKnowledgeFieldReport { + RadrootsKnowledgeFieldReport { + schema: RADROOTS_KNOWLEDGE_FIELD_REPORT_SCHEMA.to_string(), + schema_version: RADROOTS_KNOWLEDGE_SCHEMA_VERSION, + report_type: "observation".to_string(), + title: "Field observation".to_string(), + summary: Some("Observed cover crop residue.".to_string()), + context: RadrootsKnowledgeFieldContext { + location_precision: RadrootsKnowledgeLocationPrecision::CoarseGeohash, + public_location: Some(RadrootsKnowledgeLocation { + label: Some("watershed".to_string()), + region: Some("synthetic-region".to_string()), + locality: None, + geohash: Some("c23".to_string()), + }), + private_location_ref: None, + topics: vec!["field".to_string()], + context_tags: vec!["observation".to_string()], + }, + observations: vec![RadrootsKnowledgeObservation { + observation_type: "residue".to_string(), + text: "Residue was visible across beds.".to_string(), + observed_at: Some("2026-07-05".to_string()), + values: vec![RadrootsKnowledgeObservationValue { + key: "coverage".to_string(), + value: "medium".to_string(), + unit: None, + }], + }], + artifact_refs: vec![knowledge_event_ref('c', KIND_FILE_METADATA)], + related_refs: vec![knowledge_event_ref('d', KIND_KNOWLEDGE_CLAIM)], + limitations: vec!["single observer".to_string()], + } + } + + fn signed_claim_event_json() -> String { + let claim_json = serde_json::to_string(&sample_knowledge_claim()).expect("claim json"); + let tags = serde_json::from_str::<Vec<Vec<String>>>( + &knowledge_claim_tags(&claim_json).expect("claim tags"), + ) + .expect("tags"); + let tags = tags + .into_iter() + .map(nostr::Tag::parse) + .collect::<Result<Vec<_>, _>>() + .expect("parsed tags"); + let keys = + nostr::Keys::parse("0101010101010101010101010101010101010101010101010101010101010101") + .expect("keys"); + let event = + nostr::EventBuilder::new(nostr::Kind::Custom(KIND_KNOWLEDGE_CLAIM as u16), claim_json) + .tags(tags) + .custom_created_at(nostr::Timestamp::from_secs(1_800_000_000)) + .sign_with_keys(&keys) + .expect("signed event"); + serde_json::to_string(&radroots_events::RadrootsNostrEvent { + id: event.id.to_hex(), + author: event.pubkey.to_hex(), + created_at: event.created_at.as_secs() as u32, + kind: u32::from(event.kind.as_u16()), + tags: event + .tags + .as_slice() + .iter() + .map(|tag| tag.as_slice().to_vec()) + .collect(), + content: event.content, + sig: event.sig.to_string(), + }) + .expect("event json") + } + fn social_location() -> RadrootsSocialLocation { RadrootsSocialLocation { name: Some("field edge".to_string()), @@ -989,6 +1384,98 @@ mod tests { } #[test] + fn knowledge_bindings_encode_to_json_when_input_is_valid() { + let article_tags = tags_json(wiki_article_tags( + &serde_json::to_string(&sample_wiki_article()).expect("wiki article json"), + )); + assert!(has_tag(&article_tags, "title", "Soil health")); + + let redirect_tags = tags_json(wiki_redirect_tags( + &serde_json::to_string(&sample_wiki_redirect()).expect("wiki redirect json"), + )); + assert!( + redirect_tags + .iter() + .any(|tag| tag.first() == Some(&"a".to_string())) + ); + + assert_tags_json(wiki_merge_request_tags( + &serde_json::to_string(&sample_wiki_merge_request()).expect("merge request json"), + )); + + let source_tags = tags_json(knowledge_source_tags( + &serde_json::to_string(&sample_knowledge_source()).expect("source json"), + )); + assert!(has_tag( + &source_tags, + "contract", + RADROOTS_KNOWLEDGE_SOURCE_SCHEMA + )); + + let claim_tags = tags_json(knowledge_claim_tags( + &serde_json::to_string(&sample_knowledge_claim()).expect("claim json"), + )); + assert!(has_tag( + &claim_tags, + "contract", + RADROOTS_KNOWLEDGE_CLAIM_SCHEMA + )); + + assert_tags_json(knowledge_relation_tags( + &serde_json::to_string(&sample_knowledge_relation()).expect("relation json"), + )); + assert_tags_json(knowledge_review_tags( + &serde_json::to_string(&sample_knowledge_review()).expect("review json"), + )); + assert_tags_json(knowledge_field_report_tags( + &serde_json::to_string(&sample_knowledge_field_report()).expect("field report json"), + )); + } + + #[test] + fn knowledge_bindings_verify_decode_and_manifest_json() { + let decoded = verify_and_decode_event_json(&signed_claim_event_json()).expect("decoded"); + let decoded: serde_json::Value = serde_json::from_str(&decoded).expect("decoded json"); + assert_eq!(decoded["event_type"], "knowledge_claim"); + assert_eq!(decoded["contract_id"], RADROOTS_KNOWLEDGE_CLAIM_SCHEMA); + assert_eq!( + decoded["payload"]["text"], + "Cover crops improve soil structure." + ); + + let manifest = contract_manifest_json().expect("manifest"); + let manifest: serde_json::Value = serde_json::from_str(&manifest).expect("manifest json"); + assert_eq!(manifest["schema_version"], 1); + assert_eq!(manifest["contract_count"], 11); + assert!( + manifest["contracts"] + .as_array() + .expect("contracts") + .iter() + .any(|contract| contract["contract_id"] == RADROOTS_KNOWLEDGE_CLAIM_SCHEMA) + ); + } + + #[test] + fn knowledge_bindings_verify_decode_errors_are_coded_json() { + let invalid_json = verify_and_decode_event_json("not json").expect_err("invalid json"); + let invalid_json: serde_json::Value = + serde_json::from_str(&invalid_json).expect("error json"); + assert_eq!(invalid_json["code"], "invalid_json"); + assert_eq!(invalid_json["inner_code"], "event_json"); + + let mut event: serde_json::Value = + serde_json::from_str(&signed_claim_event_json()).expect("event json"); + event["sig"] = serde_json::Value::String("0".repeat(128)); + let signature_error = + verify_and_decode_event_json(&event.to_string()).expect_err("signature error"); + let signature_error: serde_json::Value = + serde_json::from_str(&signature_error).expect("signature error json"); + assert_eq!(signature_error["code"], "nip01_verification"); + assert_eq!(signature_error["inner_code"], "signature_invalid"); + } + + #[test] fn field_bindings_encode_to_json_when_input_is_valid() { let workspace_json = serde_json::to_string(&sample_workspace_manifest()).expect("workspace json"); diff --git a/packages/events-bindings/src/generated/kinds.ts b/packages/events-bindings/src/generated/kinds.ts @@ -20,6 +20,12 @@ export const KIND_COMMENT = 1111; export const KIND_GEOCHAT = 20000; +export const KIND_WIKI_MERGE_REQUEST = 818; + +export const KIND_WIKI_ARTICLE = 30818; + +export const KIND_WIKI_REDIRECT = 30819; + export const KIND_LIST_MUTE = 10000; export const KIND_LIST_PINNED_NOTES = 10001; @@ -138,6 +144,22 @@ export const KIND_TRADE_RECEIPT = 3434; export const KIND_TRADE_VALIDATION_RECEIPT = 3440; +export const KIND_KNOWLEDGE_CLAIM = 3460; + +export const KIND_KNOWLEDGE_RELATION = 3461; + +export const KIND_KNOWLEDGE_REVIEW = 3462; + +export const KIND_KNOWLEDGE_FIELD_REPORT = 3463; + +export const KIND_KNOWLEDGE_CHANGE_PROPOSAL = 3464; + +export const KIND_CONTRIBUTION_ATTESTATION = 3465; + +export const KIND_KNOWLEDGE_SOURCE = 30450; + +export const KIND_EVIDENCE_BOUNTY = 30451; + export const KIND_JOB_REQUEST_MIN = 5000; export const KIND_JOB_REQUEST_MAX = 5999; diff --git a/packages/events-bindings/src/generated/types.ts b/packages/events-bindings/src/generated/types.ts @@ -10,12 +10,16 @@ export type JobPaymentRequest = { amount_sat: number, bolt11?: string | null, }; export type RadrootsAccountClaim = { username: string, pubkey: string, nip05?: string | null, }; +export type RadrootsAddressableRef = { kind: number, pubkey: string, d_tag: string, relays: Array<string>, }; + export type RadrootsAppData = { d_tag: string, content: string, }; export type RadrootsComment = { root: RadrootsSocialTarget, parent: RadrootsSocialTarget, content: string, }; export type RadrootsCommercialDomain = "trade:listing"; +export type RadrootsContributionAttestation = { schema: string, schema_version: number, contributor_pubkey: string, contribution_type: string, subject_refs: Array<RadrootsNostrEventRef>, summary: string, evidence_refs: Array<RadrootsNostrEventRef>, }; + export type RadrootsCoop = { d_tag: string, name: string, about?: string | null, website?: string | null, picture?: string | null, banner?: string | null, location?: RadrootsCoopLocation | null, tags?: Array<string> | null, }; export type RadrootsCoopLocation = { primary?: string | null, city?: string | null, region?: string | null, country?: string | null, gcs: RadrootsGcsLocation, }; @@ -26,6 +30,8 @@ export type RadrootsDocument = { d_tag: string, doc_type: string, title: string, export type RadrootsDocumentSubject = { pubkey: string, address?: string | null, }; +export type RadrootsEvidenceBounty = { schema: string, schema_version: number, d_tag: string, title: string, summary?: string | null, topics: Array<string>, target_refs: Array<RadrootsNostrEventRef>, reward_note?: string | null, closes_at?: string | null, }; + export type RadrootsFarm = { d_tag: string, name: string, about?: string | null, website?: string | null, picture?: string | null, banner?: string | null, location?: RadrootsFarmPublicLocation | null, tags?: Array<string> | null, }; export type RadrootsFarmPublicLocation = { primary: string, city?: string | null, region?: string | null, country?: string | null, geohash: string, }; @@ -58,6 +64,38 @@ export type RadrootsJobRequest = { kind: number, inputs: Array<RadrootsJobInput> export type RadrootsJobResult = { kind: number, request_event: RadrootsNostrEventPtr, request_json?: string | null, inputs: Array<RadrootsJobInput>, customer_pubkey?: string | null, payment?: JobPaymentRequest | null, content?: string | null, encrypted: boolean, }; +export type RadrootsKnowledgeChangeProposal = { schema: string, schema_version: number, target: RadrootsNostrEventRef, proposal_type: string, summary: string, rationale?: string | null, evidence_refs: Array<RadrootsNostrEventRef>, supersedes: Array<RadrootsNostrEventRef>, }; + +export type RadrootsKnowledgeCitationSpan = { source_ref: RadrootsNostrEventRef, artifact_ref?: RadrootsNostrEventRef | null, page_start?: number | null, page_end?: number | null, section_path: Array<string>, quote_hash?: string | null, chunk_id?: string | null, }; + +export type RadrootsKnowledgeClaim = { schema: string, schema_version: number, claim_type: string, text: string, citation_spans: Array<RadrootsKnowledgeCitationSpan>, topics: Array<string>, applies_to: Array<string>, author_asserted_confidence?: string | null, supersedes: Array<RadrootsNostrEventRef>, }; + +export type RadrootsKnowledgeFieldContext = { location_precision: RadrootsKnowledgeLocationPrecision, public_location?: RadrootsKnowledgeLocation | null, private_location_ref?: RadrootsNostrEventRef | null, topics: Array<string>, context_tags: Array<string>, }; + +export type RadrootsKnowledgeFieldReport = { schema: string, schema_version: number, report_type: string, title: string, summary?: string | null, context: RadrootsKnowledgeFieldContext, observations: Array<RadrootsKnowledgeObservation>, artifact_refs: Array<RadrootsNostrEventRef>, related_refs: Array<RadrootsNostrEventRef>, limitations: Array<string>, }; + +export type RadrootsKnowledgeLocation = { label?: string | null, region?: string | null, locality?: string | null, geohash?: string | null, }; + +export type RadrootsKnowledgeLocationPrecision = "none" | "region" | "locality" | "coarse_geohash" | "exact_public" | "exact_private_reference"; + +export type RadrootsKnowledgeNodeRef = { node_type: string, event_ref?: RadrootsNostrEventRef | null, address_ref?: RadrootsAddressableRef | null, external_id?: string | null, label?: string | null, }; + +export type RadrootsKnowledgeObservation = { observation_type: string, text: string, observed_at?: string | null, values: Array<RadrootsKnowledgeObservationValue>, }; + +export type RadrootsKnowledgeObservationValue = { key: string, value: string, unit?: string | null, }; + +export type RadrootsKnowledgeRelation = { schema: string, schema_version: number, subject: RadrootsKnowledgeNodeRef, predicate: string, object: RadrootsKnowledgeNodeRef, support_refs: Array<RadrootsNostrEventRef>, author_asserted_confidence?: string | null, supersedes: Array<RadrootsNostrEventRef>, }; + +export type RadrootsKnowledgeReview = { schema: string, schema_version: number, target: RadrootsKnowledgeReviewTarget, reviewer_role: string, verdict: string, scores: Array<RadrootsKnowledgeReviewScore>, notes?: string | null, evidence_refs: Array<RadrootsNostrEventRef>, }; + +export type RadrootsKnowledgeReviewScope = "specific_version" | "addressable_coordinate_at_published_at" | "policy_latest"; + +export type RadrootsKnowledgeReviewScore = { dimension: string, value: string, note?: string | null, }; + +export type RadrootsKnowledgeReviewTarget = { event_id: string, author_pubkey: string, kind: number, address?: string | null, relays: Array<string>, review_scope: RadrootsKnowledgeReviewScope, }; + +export type RadrootsKnowledgeSource = { schema: string, schema_version: number, d_tag: string, title: string, source_type: string, authors: Array<string>, publisher?: string | null, publication_year?: number | null, edition?: string | null, canonical_url?: string | null, artifact_refs: Array<RadrootsNostrEventRef>, author_asserted_rights?: RadrootsRightsAssertion | null, topics: Array<string>, summary?: string | null, }; + export type RadrootsList = { content: string, entries: Array<RadrootsListEntry>, }; export type RadrootsListEntry = { tag: string, values: Array<string>, }; @@ -162,6 +200,8 @@ export type RadrootsResourceHarvestCap = { d_tag: string, resource_area: Radroot export type RadrootsResourceHarvestProduct = { key: string, category?: string | null, }; +export type RadrootsRightsAssertion = { assertion: string, holder?: string | null, license?: string | null, url?: string | null, }; + export type RadrootsSeal = { content: string, }; export type RadrootsSocialFarmAnchor = { farm: RadrootsFarmRef, relays?: Array<string> | null, }; @@ -181,3 +221,9 @@ export type RadrootsTradeListingValidateRequest = { listing_event?: RadrootsNost export type RadrootsTradeListingValidateResult = { valid: boolean, errors: Array<RadrootsTradeValidationListingError>, }; export type RadrootsTradeValidationListingError = { kind: "invalid_kind", amount: { kind: number, }, } | { kind: "missing_listing_id", } | { kind: "listing_event_not_found", amount: { listing_addr: string, }, } | { kind: "listing_event_fetch_failed", amount: { listing_addr: string, }, } | { kind: "parse_error", amount: { error: RadrootsListingParseError, }, } | { kind: "invalid_seller", } | { kind: "missing_farm_profile", } | { kind: "missing_farm_record", } | { kind: "missing_title", } | { kind: "missing_description", } | { kind: "missing_product_type", } | { kind: "missing_bins", } | { kind: "missing_primary_bin", } | { kind: "invalid_bin", } | { kind: "missing_price", } | { kind: "invalid_price", } | { kind: "missing_inventory", } | { kind: "invalid_inventory", } | { kind: "missing_availability", } | { kind: "missing_location", } | { kind: "missing_location_locality", } | { kind: "missing_location_geohash", } | { kind: "invalid_location_geohash", } | { kind: "missing_delivery_method", }; + +export type RadrootsWikiArticle = { d_tag: string, title: string, content_djot: string, summary?: string | null, topics: Array<string>, references: Array<RadrootsNostrEventRef>, forked_from: Array<RadrootsNostrEventRef>, deferred_to?: RadrootsNostrEventRef | null, }; + +export type RadrootsWikiMergeRequest = { target_article: RadrootsAddressableRef, destination_pubkey: string, base_version_event_id?: string | null, source_version_event_id: string, explanation?: string | null, }; + +export type RadrootsWikiRedirect = { d_tag: string, target: RadrootsNostrEventRef, }; diff --git a/tools/xtask/src/dto_roots.rs b/tools/xtask/src/dto_roots.rs @@ -590,14 +590,17 @@ mod tests { "JobInputType", "JobPaymentRequest", "RadrootsAccountClaim", + "RadrootsAddressableRef", "RadrootsAppData", "RadrootsComment", "RadrootsCommercialDomain", + "RadrootsContributionAttestation", "RadrootsCoop", "RadrootsCoopLocation", "RadrootsCoopRef", "RadrootsDocument", "RadrootsDocumentSubject", + "RadrootsEvidenceBounty", "RadrootsFarm", "RadrootsFarmPublicLocation", "RadrootsFarmRef", @@ -614,6 +617,22 @@ mod tests { "RadrootsJobParam", "RadrootsJobRequest", "RadrootsJobResult", + "RadrootsKnowledgeChangeProposal", + "RadrootsKnowledgeCitationSpan", + "RadrootsKnowledgeClaim", + "RadrootsKnowledgeFieldContext", + "RadrootsKnowledgeFieldReport", + "RadrootsKnowledgeLocation", + "RadrootsKnowledgeLocationPrecision", + "RadrootsKnowledgeNodeRef", + "RadrootsKnowledgeObservation", + "RadrootsKnowledgeObservationValue", + "RadrootsKnowledgeRelation", + "RadrootsKnowledgeReview", + "RadrootsKnowledgeReviewScope", + "RadrootsKnowledgeReviewScore", + "RadrootsKnowledgeReviewTarget", + "RadrootsKnowledgeSource", "RadrootsList", "RadrootsListEntry", "RadrootsListSet", @@ -666,6 +685,7 @@ mod tests { "RadrootsResourceAreaRef", "RadrootsResourceHarvestCap", "RadrootsResourceHarvestProduct", + "RadrootsRightsAssertion", "RadrootsSeal", "RadrootsSocialFarmAnchor", "RadrootsSocialLocation", @@ -676,6 +696,9 @@ mod tests { "RadrootsTradeListingValidateRequest", "RadrootsTradeListingValidateResult", "RadrootsTradeValidationListingError", + "RadrootsWikiArticle", + "RadrootsWikiMergeRequest", + "RadrootsWikiRedirect", ]; const EVENTS_INDEXED_TYPE_INVENTORY: &[&str] = &[ "RadrootsEventsIndexedShardId", diff --git a/tools/xtask/src/wasm_declarations.rs b/tools/xtask/src/wasm_declarations.rs @@ -393,6 +393,23 @@ const EVENTS_CODEC_FUNCTIONS: &[WasmPublicFunction] = &[ public_function!("job_feedback_tags", Some("job_json"), String, 2, 4), public_function!("job_request_tags", Some("job_json"), String, 2, 4), public_function!("job_result_tags", Some("job_json"), String, 2, 4), + public_function!("knowledge_claim_tags", Some("claim_json"), String, 2, 4), + public_function!( + "knowledge_field_report_tags", + Some("report_json"), + String, + 2, + 4 + ), + public_function!( + "knowledge_relation_tags", + Some("relation_json"), + String, + 2, + 4 + ), + public_function!("knowledge_review_tags", Some("review_json"), String, 2, 4), + public_function!("knowledge_source_tags", Some("source_json"), String, 2, 4), public_function!("list_set_tags", Some("list_json"), String, 2, 4), public_function!("list_tags", Some("list_json"), String, 2, 4), public_function!("listing_tags", Some("listing_json"), String, 2, 4), @@ -406,6 +423,23 @@ const EVENTS_CODEC_FUNCTIONS: &[WasmPublicFunction] = &[ public_function!("report_tags", Some("report_json"), String, 2, 4), public_function!("repost_tags", Some("repost_json"), String, 2, 4), public_function!("seal_tags", Some("seal_json"), String, 2, 4), + public_function!("wiki_article_tags", Some("article_json"), String, 2, 4), + public_function!( + "wiki_merge_request_tags", + Some("request_json"), + String, + 2, + 4 + ), + public_function!("wiki_redirect_tags", Some("redirect_json"), String, 2, 4), + public_function!( + "verify_and_decode_event_json", + Some("event_json"), + String, + 2, + 4 + ), + public_function!("contract_manifest_json", None, String, 0, 4), ]; const EVENTS_CODEC_SUPPORT_EXPORTS: &[WasmSupportExport] = &[