commit fc71e707191ca282364f944541a968c14e8c0010
parent 602a9095572f9f2c02815104b5559b44c5cb326b
Author: triesap <tyson@radroots.org>
Date: Mon, 20 Jul 2026 00:55:23 +0000
sdk: align foundational event workflows
- adopt current Operational Listing and verified event-store owner APIs
- expose strict authored social and exact-byte Blossom bindings
- enforce verified signer, ingestion, and structured validation boundaries
- seal prepared publish plans and rederive receipt identities
Diffstat:
51 files changed, 2659 insertions(+), 1095 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -1834,6 +1834,7 @@ name = "radroots_event_codec_wasm"
version = "0.1.0-alpha.2"
dependencies = [
"nostr",
+ "radroots_blossom",
"radroots_core",
"radroots_event",
"radroots_event_codec",
@@ -1864,7 +1865,7 @@ version = "1.0.0-alpha.1"
dependencies = [
"hex",
"radroots_event",
- "radroots_nostr",
+ "radroots_event_codec",
"radroots_transport",
"serde",
"serde_json",
diff --git a/Cargo.toml b/Cargo.toml
@@ -29,8 +29,9 @@ readme = "README"
dto_bindgen = { version = "0.1.0" }
dto_bindgen_backend_ts = { version = "0.1.0" }
dto_bindgen_core = { version = "0.1.0" }
-radroots_core = { path = "../lib/crates/core", version = "=1.0.0-alpha.1", default-features = false }
radroots_authority = { path = "../lib/crates/authority", version = "=1.0.0-alpha.1", default-features = false }
+radroots_blossom = { path = "../lib/crates/blossom", version = "=1.0.0-alpha.1", default-features = false }
+radroots_core = { path = "../lib/crates/core", version = "=1.0.0-alpha.1", default-features = false }
radroots_event_store = { path = "../lib/crates/event_store", version = "=1.0.0-alpha.1", default-features = false }
radroots_event = { path = "../lib/crates/event", version = "=1.0.0-alpha.1", default-features = false }
radroots_event_codec = { path = "../lib/crates/event_codec", version = "=1.0.0-alpha.1", default-features = false }
diff --git a/contracts/packages/go.toml b/contracts/packages/go.toml
@@ -14,15 +14,17 @@ order = 3
[operations]
"farm.build_draft" = "farm.BuildDraft"
-"listing.build_tags" = "listing.BuildTags"
-"listing.build_draft" = "listing.BuildDraft"
-"listing.parse_event" = "listing.ParseEvent"
-"social.post.build_tags" = "social.PostBuildTags"
-"social.comment.build_tags" = "social.CommentBuildTags"
+"operational_listing.build_tags" = "operational_listing.BuildTags"
+"operational_listing.build_draft" = "operational_listing.BuildDraft"
+"operational_listing.parse_event" = "operational_listing.ParseEvent"
+"social.update.build_authored_draft" = "social.UpdateBuildAuthoredDraft"
+"social.photo_update.build_authored_draft" = "social.PhotoUpdateBuildAuthoredDraft"
+"social.ask.build_authored_draft" = "social.AskBuildAuthoredDraft"
+"social.comment.build_authored_draft" = "social.CommentBuildAuthoredDraft"
"social.reaction.build_tags" = "social.ReactionBuildTags"
"social.article.build_tags" = "social.ArticleBuildTags"
"social.file_metadata.build_tags" = "social.FileMetadataBuildTags"
-"trade_validation.validate_listing_event" = "tradevalidation.ValidateListingEvent"
+"trade_validation.validate_operational_listing_event" = "tradevalidation.ValidateOperationalListingEvent"
[shared_types]
"RadrootsNip01EventWireParts" = "RadrootsNip01EventWireParts"
@@ -33,11 +35,10 @@ order = 3
"RadrootsEventEnvelopeDto" = "RadrootsEventEnvelopeDto"
"RadrootsEventRef" = "RadrootsEventRef"
"RadrootsEventPtr" = "RadrootsEventPtr"
-"RadrootsListingAddress" = "ListingAddress"
+"RadrootsClassifiedListingAddress" = "ClassifiedListingAddress"
"RadrootsFarm" = "RadrootsFarm"
-"RadrootsListing" = "RadrootsListing"
+"RadrootsOperationalListing" = "RadrootsOperationalListing"
"RadrootsPost" = "RadrootsPost"
-"RadrootsComment" = "RadrootsComment"
"RadrootsReaction" = "RadrootsReaction"
"RadrootsArticle" = "RadrootsArticle"
"RadrootsFileMetadata" = "RadrootsFileMetadata"
diff --git a/contracts/packages/kotlin.toml b/contracts/packages/kotlin.toml
@@ -14,15 +14,17 @@ order = 2
[operations]
"farm.build_draft" = "farm.buildDraft"
-"listing.build_tags" = "listing.buildTags"
-"listing.build_draft" = "listing.buildDraft"
-"listing.parse_event" = "listing.parseEvent"
-"social.post.build_tags" = "social.post.buildTags"
-"social.comment.build_tags" = "social.comment.buildTags"
+"operational_listing.build_tags" = "operationalListing.buildTags"
+"operational_listing.build_draft" = "operationalListing.buildDraft"
+"operational_listing.parse_event" = "operationalListing.parseEvent"
+"social.update.build_authored_draft" = "social.update.buildAuthoredDraft"
+"social.photo_update.build_authored_draft" = "social.photoUpdate.buildAuthoredDraft"
+"social.ask.build_authored_draft" = "social.ask.buildAuthoredDraft"
+"social.comment.build_authored_draft" = "social.comment.buildAuthoredDraft"
"social.reaction.build_tags" = "social.reaction.buildTags"
"social.article.build_tags" = "social.article.buildTags"
"social.file_metadata.build_tags" = "social.fileMetadata.buildTags"
-"trade_validation.validate_listing_event" = "tradeValidation.validateListingEvent"
+"trade_validation.validate_operational_listing_event" = "tradeValidation.validateOperationalListingEvent"
[shared_types]
"RadrootsNip01EventWireParts" = "RadrootsNip01EventWireParts"
@@ -33,11 +35,10 @@ order = 2
"RadrootsEventEnvelopeDto" = "RadrootsEventEnvelopeDto"
"RadrootsEventRef" = "RadrootsEventRef"
"RadrootsEventPtr" = "RadrootsEventPtr"
-"RadrootsListingAddress" = "ListingAddress"
+"RadrootsClassifiedListingAddress" = "ClassifiedListingAddress"
"RadrootsFarm" = "RadrootsFarm"
-"RadrootsListing" = "RadrootsListing"
+"RadrootsOperationalListing" = "RadrootsOperationalListing"
"RadrootsPost" = "RadrootsPost"
-"RadrootsComment" = "RadrootsComment"
"RadrootsReaction" = "RadrootsReaction"
"RadrootsArticle" = "RadrootsArticle"
"RadrootsFileMetadata" = "RadrootsFileMetadata"
diff --git a/contracts/packages/py.toml b/contracts/packages/py.toml
@@ -14,15 +14,17 @@ order = 3
[operations]
"farm.build_draft" = "farm_build_draft"
-"listing.build_tags" = "listing_build_tags"
-"listing.build_draft" = "listing_build_draft"
-"listing.parse_event" = "listing_parse_event"
-"social.post.build_tags" = "social_post_build_tags"
-"social.comment.build_tags" = "social_comment_build_tags"
+"operational_listing.build_tags" = "operational_listing_build_tags"
+"operational_listing.build_draft" = "operational_listing_build_draft"
+"operational_listing.parse_event" = "operational_listing_parse_event"
+"social.update.build_authored_draft" = "social_update_build_authored_draft"
+"social.photo_update.build_authored_draft" = "social_photo_update_build_authored_draft"
+"social.ask.build_authored_draft" = "social_ask_build_authored_draft"
+"social.comment.build_authored_draft" = "social_comment_build_authored_draft"
"social.reaction.build_tags" = "social_reaction_build_tags"
"social.article.build_tags" = "social_article_build_tags"
"social.file_metadata.build_tags" = "social_file_metadata_build_tags"
-"trade_validation.validate_listing_event" = "trade_validation_validate_listing_event"
+"trade_validation.validate_operational_listing_event" = "trade_validation_validate_operational_listing_event"
[shared_types]
"RadrootsNip01EventWireParts" = "RadrootsNip01EventWireParts"
@@ -33,11 +35,10 @@ order = 3
"RadrootsEventEnvelopeDto" = "RadrootsEventEnvelopeDto"
"RadrootsEventRef" = "RadrootsEventRef"
"RadrootsEventPtr" = "RadrootsEventPtr"
-"RadrootsListingAddress" = "ListingAddress"
+"RadrootsClassifiedListingAddress" = "ClassifiedListingAddress"
"RadrootsFarm" = "RadrootsFarm"
-"RadrootsListing" = "RadrootsListing"
+"RadrootsOperationalListing" = "RadrootsOperationalListing"
"RadrootsPost" = "RadrootsPost"
-"RadrootsComment" = "RadrootsComment"
"RadrootsReaction" = "RadrootsReaction"
"RadrootsArticle" = "RadrootsArticle"
"RadrootsFileMetadata" = "RadrootsFileMetadata"
diff --git a/contracts/packages/swift.toml b/contracts/packages/swift.toml
@@ -14,15 +14,17 @@ order = 2
[operations]
"farm.build_draft" = "farm.buildDraft"
-"listing.build_tags" = "listing.buildTags"
-"listing.build_draft" = "listing.buildDraft"
-"listing.parse_event" = "listing.parseEvent"
-"social.post.build_tags" = "social.post.buildTags"
-"social.comment.build_tags" = "social.comment.buildTags"
+"operational_listing.build_tags" = "operationalListing.buildTags"
+"operational_listing.build_draft" = "operationalListing.buildDraft"
+"operational_listing.parse_event" = "operationalListing.parseEvent"
+"social.update.build_authored_draft" = "social.update.buildAuthoredDraft"
+"social.photo_update.build_authored_draft" = "social.photoUpdate.buildAuthoredDraft"
+"social.ask.build_authored_draft" = "social.ask.buildAuthoredDraft"
+"social.comment.build_authored_draft" = "social.comment.buildAuthoredDraft"
"social.reaction.build_tags" = "social.reaction.buildTags"
"social.article.build_tags" = "social.article.buildTags"
"social.file_metadata.build_tags" = "social.fileMetadata.buildTags"
-"trade_validation.validate_listing_event" = "tradeValidation.validateListingEvent"
+"trade_validation.validate_operational_listing_event" = "tradeValidation.validateOperationalListingEvent"
[shared_types]
"RadrootsNip01EventWireParts" = "RadrootsNip01EventWireParts"
@@ -33,11 +35,10 @@ order = 2
"RadrootsEventEnvelopeDto" = "RadrootsEventEnvelopeDto"
"RadrootsEventRef" = "RadrootsEventRef"
"RadrootsEventPtr" = "RadrootsEventPtr"
-"RadrootsListingAddress" = "ListingAddress"
+"RadrootsClassifiedListingAddress" = "ClassifiedListingAddress"
"RadrootsFarm" = "RadrootsFarm"
-"RadrootsListing" = "RadrootsListing"
+"RadrootsOperationalListing" = "RadrootsOperationalListing"
"RadrootsPost" = "RadrootsPost"
-"RadrootsComment" = "RadrootsComment"
"RadrootsReaction" = "RadrootsReaction"
"RadrootsArticle" = "RadrootsArticle"
"RadrootsFileMetadata" = "RadrootsFileMetadata"
diff --git a/contracts/packages/ts.toml b/contracts/packages/ts.toml
@@ -84,15 +84,17 @@ order = 1
[operations]
"farm.build_draft" = "farm.buildDraft"
-"listing.build_tags" = "listing.buildTags"
-"listing.build_draft" = "listing.buildDraft"
-"listing.parse_event" = "listing.parseEvent"
-"social.post.build_tags" = "social.post.buildTags"
-"social.comment.build_tags" = "social.comment.buildTags"
+"operational_listing.build_tags" = "operationalListing.buildTags"
+"operational_listing.build_draft" = "operationalListing.buildDraft"
+"operational_listing.parse_event" = "operationalListing.parseEvent"
+"social.update.build_authored_draft" = "social.update.buildAuthoredDraft"
+"social.photo_update.build_authored_draft" = "social.photoUpdate.buildAuthoredDraft"
+"social.ask.build_authored_draft" = "social.ask.buildAuthoredDraft"
+"social.comment.build_authored_draft" = "social.comment.buildAuthoredDraft"
"social.reaction.build_tags" = "social.reaction.buildTags"
"social.article.build_tags" = "social.article.buildTags"
"social.file_metadata.build_tags" = "social.fileMetadata.buildTags"
-"trade_validation.validate_listing_event" = "tradeValidation.validateListingEvent"
+"trade_validation.validate_operational_listing_event" = "tradeValidation.validateOperationalListingEvent"
[shared_types]
"RadrootsNip01EventWireParts" = "RadrootsNip01EventWireParts"
@@ -103,11 +105,10 @@ order = 1
"RadrootsEventEnvelopeDto" = "RadrootsEventEnvelopeDto"
"RadrootsEventRef" = "RadrootsEventRef"
"RadrootsEventPtr" = "RadrootsEventPtr"
-"RadrootsListingAddress" = "ListingAddress"
+"RadrootsClassifiedListingAddress" = "ClassifiedListingAddress"
"RadrootsFarm" = "RadrootsFarm"
-"RadrootsListing" = "RadrootsListing"
+"RadrootsOperationalListing" = "RadrootsOperationalListing"
"RadrootsPost" = "RadrootsPost"
-"RadrootsComment" = "RadrootsComment"
"RadrootsReaction" = "RadrootsReaction"
"RadrootsArticle" = "RadrootsArticle"
"RadrootsFileMetadata" = "RadrootsFileMetadata"
diff --git a/crates/event_bindings/src/lib.rs b/crates/event_bindings/src/lib.rs
@@ -6,7 +6,9 @@ pub use model::{constants_module, kinds_module};
#[cfg(test)]
mod tests {
- use radroots_event::{kinds, listing::RADROOTS_LISTING_PRODUCT_TAG_KEYS};
+ use radroots_event::{
+ kinds, operational_listing::RADROOTS_OPERATIONAL_LISTING_PRODUCT_TAG_KEYS,
+ };
use super::{constants_module, kinds_module};
@@ -14,10 +16,10 @@ mod tests {
fn preserves_event_constant_exports() {
let constants = constants_module().render_source();
let kinds_ts = kinds_module().render_source();
- assert!(constants.contains("RADROOTS_LISTING_PRODUCT_TAG_KEYS"));
- assert!(constants.contains(RADROOTS_LISTING_PRODUCT_TAG_KEYS[0]));
- assert!(kinds_ts.contains("KIND_LISTING"));
- assert!(kinds_ts.contains(&kinds::KIND_LISTING.to_string()));
+ assert!(constants.contains("RADROOTS_OPERATIONAL_LISTING_PRODUCT_TAG_KEYS"));
+ assert!(constants.contains(RADROOTS_OPERATIONAL_LISTING_PRODUCT_TAG_KEYS[0]));
+ assert!(kinds_ts.contains("KIND_CLASSIFIED_LISTING"));
+ 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_ORDER_REQUEST"));
diff --git a/crates/event_bindings/src/model.rs b/crates/event_bindings/src/model.rs
@@ -1,19 +1,21 @@
use dto_bindgen_backend_ts::{
TypeScriptDeclaration, TypeScriptImport, TypeScriptModule, TypeScriptType, TypeScriptValue,
};
-use radroots_event::{kinds, listing::RADROOTS_LISTING_PRODUCT_TAG_KEYS};
+use radroots_event::{kinds, operational_listing::RADROOTS_OPERATIONAL_LISTING_PRODUCT_TAG_KEYS};
pub fn constants_module() -> TypeScriptModule {
TypeScriptModule::new("src/generated/constants.ts")
.with_import(TypeScriptImport::type_only(
- ["RadrootsListingProductTagKeys"],
+ ["RadrootsOperationalListingProductTagKeys"],
"./types.js",
))
.with_declaration(TypeScriptDeclaration::constant(
- "RADROOTS_LISTING_PRODUCT_TAG_KEYS",
- Some(TypeScriptType::named("RadrootsListingProductTagKeys")),
+ "RADROOTS_OPERATIONAL_LISTING_PRODUCT_TAG_KEYS",
+ Some(TypeScriptType::named(
+ "RadrootsOperationalListingProductTagKeys",
+ )),
TypeScriptValue::array(
- RADROOTS_LISTING_PRODUCT_TAG_KEYS
+ RADROOTS_OPERATIONAL_LISTING_PRODUCT_TAG_KEYS
.iter()
.map(|value| TypeScriptValue::string(*value))
.collect::<Vec<_>>(),
@@ -111,7 +113,7 @@ const EVENT_KIND_EXPORTS: &[(&str, u32)] = &[
),
("KIND_ACCOUNT_CLAIM", kinds::KIND_ACCOUNT_CLAIM),
("KIND_APP_DATA", kinds::KIND_APP_DATA),
- ("KIND_LISTING", kinds::KIND_LISTING),
+ ("KIND_CLASSIFIED_LISTING", kinds::KIND_CLASSIFIED_LISTING),
("KIND_APPLICATION_HANDLER", kinds::KIND_APPLICATION_HANDLER),
("KIND_TRADE_PROPOSAL", kinds::KIND_TRADE_PROPOSAL),
("KIND_TRADE_DECISION", kinds::KIND_TRADE_DECISION),
diff --git a/crates/event_codec_wasm/Cargo.toml b/crates/event_codec_wasm/Cargo.toml
@@ -15,6 +15,9 @@ readme = "README"
crate-type = ["cdylib", "rlib"]
[dependencies]
+radroots_blossom = { workspace = true, default-features = false, features = [
+ "std",
+] }
radroots_event = { workspace = true, default-features = false, features = [
"std",
"serde",
diff --git a/crates/event_codec_wasm/src/lib.rs b/crates/event_codec_wasm/src/lib.rs
@@ -1,7 +1,14 @@
#![forbid(unsafe_code)]
+use radroots_blossom::{
+ RadrootsBlossomBlobDescriptor, RadrootsBlossomBlobUrl, RadrootsBlossomError,
+ RadrootsBlossomMediaType, RadrootsBlossomSha256,
+};
use radroots_event::article::RadrootsArticle;
-use radroots_event::comment::RadrootsComment;
+use radroots_event::comment::{
+ RadrootsAuthoredNip22Comment, RadrootsNip22AddressRootReference, RadrootsNip22CommentError,
+ RadrootsNip22CommentParentReference, RadrootsNip22CommentRoot, RadrootsNip22EventRootReference,
+};
use radroots_event::coop::RadrootsCoop;
use radroots_event::document::RadrootsDocument;
use radroots_event::farm::RadrootsFarm;
@@ -28,21 +35,26 @@ use radroots_event::knowledge::{
};
use radroots_event::list::RadrootsList;
use radroots_event::list_set::RadrootsListSet;
-use radroots_event::listing::RadrootsListing;
+use radroots_event::media::{RadrootsAuthoredImage, RadrootsAuthoredImageError};
use radroots_event::message::RadrootsMessage;
use radroots_event::message_file::RadrootsMessageFile;
+use radroots_event::operational_listing::RadrootsOperationalListing;
use radroots_event::plot::RadrootsPlot;
-use radroots_event::post::RadrootsPost;
+use radroots_event::post::{
+ RadrootsAuthoredAsk, RadrootsAuthoredPhotoUpdate, RadrootsAuthoredPostError,
+ RadrootsAuthoredPostImage, RadrootsAuthoredUpdate, RadrootsPostImageDimensions,
+};
use radroots_event::reaction::RadrootsReaction;
use radroots_event::relay_auth::RadrootsRelayAuth;
use radroots_event::report::RadrootsReport;
use radroots_event::repost::{RadrootsGenericRepost, RadrootsRepost};
use radroots_event::seal::RadrootsSeal;
+use radroots_event::wire::RadrootsNip01EventWireParts;
use radroots_event::{
RadrootsEventEnvelope, RadrootsEventEnvelopeError, RadrootsEventEnvelopeParts,
};
use radroots_event_codec::article::encode::article_build_tags;
-use radroots_event_codec::comment::encode::comment_build_tags;
+use radroots_event_codec::comment::authored::authored_nip22_comment_to_wire_parts;
use radroots_event_codec::coop::encode::coop_build_tags;
use radroots_event_codec::document::encode::document_build_tags;
use radroots_event_codec::farm::encode::farm_build_tags;
@@ -70,13 +82,16 @@ use radroots_event_codec::knowledge::{
};
use radroots_event_codec::list::encode::list_build_tags;
use radroots_event_codec::list_set::encode::list_set_build_tags;
-use radroots_event_codec::listing::tags::{
- listing_tags as listing_tags_impl, listing_tags_full as listing_tags_full_impl,
-};
use radroots_event_codec::message::encode::message_build_tags;
use radroots_event_codec::message_file::encode::message_file_build_tags;
+use radroots_event_codec::operational_listing::tags::{
+ operational_listing_tags as operational_listing_tags_impl,
+ operational_listing_tags_full as operational_listing_tags_full_impl,
+};
use radroots_event_codec::plot::encode::plot_build_tags;
-use radroots_event_codec::post::encode::post_build_tags;
+use radroots_event_codec::post::authored::{
+ authored_ask_to_wire_parts, authored_photo_update_to_wire_parts, authored_update_to_wire_parts,
+};
use radroots_event_codec::reaction::encode::reaction_build_tags;
use radroots_event_codec::relay_auth::encode::relay_auth_build_tags;
use radroots_event_codec::report::encode::report_build_tags;
@@ -123,6 +138,35 @@ fn parse_json<T: DeserializeOwned>(input: &str) -> Result<T, RadrootsJsValue> {
serde_json::from_str(normalized_payload(input)).map_err(err_js)
}
+fn parse_authored_json<T: DeserializeOwned>(input: &str) -> Result<T, RadrootsJsValue> {
+ serde_json::from_str(input).map_err(|_| error_json("invalid_json", Some("authored_input")))
+}
+
+fn authored_error(code: &str) -> RadrootsJsValue {
+ error_json("encode_error", Some(code))
+}
+
+fn blossom_authored_error(error: RadrootsBlossomError) -> RadrootsJsValue {
+ authored_error(error.code())
+}
+
+fn post_authored_error(error: RadrootsAuthoredPostError) -> RadrootsJsValue {
+ authored_error(error.code())
+}
+
+fn image_authored_error(error: RadrootsAuthoredImageError) -> RadrootsJsValue {
+ authored_error(error.code())
+}
+
+fn comment_authored_error(error: RadrootsNip22CommentError) -> RadrootsJsValue {
+ authored_error(error.code())
+}
+
+fn wire_parts_to_json(parts: &RadrootsNip01EventWireParts) -> Result<String, RadrootsJsValue> {
+ serde_json::to_string(parts)
+ .map_err(|_| error_json("internal_error", Some("wire_parts_serialization")))
+}
+
fn tags_to_json(tags: Vec<Vec<String>>) -> Result<String, RadrootsJsValue> {
serde_json::to_string(&tags).map_err(err_js)
}
@@ -152,6 +196,7 @@ fn envelope_error_code(error: &RadrootsEventEnvelopeError) -> &'static str {
RadrootsEventEnvelopeError::ControlCharacterTagKey { .. } => "tag_key_control_character",
RadrootsEventEnvelopeError::ContentTooLarge { .. } => "content_too_large",
RadrootsEventEnvelopeError::TooManyTags { .. } => "too_many_tags",
+ RadrootsEventEnvelopeError::TooManyTagElements { .. } => "too_many_tag_elements",
RadrootsEventEnvelopeError::TagElementTooLarge { .. } => "tag_element_too_large",
RadrootsEventEnvelopeError::TagsTooLarge { .. } => "tags_too_large",
}
@@ -287,24 +332,247 @@ struct FarmCrdtTagsInput {
author_pubkey: String,
}
-#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = listing_tags))]
-pub fn listing_tags(listing_json: &str) -> Result<String, RadrootsJsValue> {
- build_tags_json::<RadrootsListing, _, _>(listing_json, listing_tags_impl)
+#[derive(Deserialize)]
+#[serde(deny_unknown_fields)]
+struct AuthoredUpdateInput {
+ content: String,
+}
+
+#[derive(Deserialize)]
+#[serde(deny_unknown_fields)]
+struct AuthoredMediaPostInput {
+ content: String,
+ #[serde(default)]
+ images: Vec<AuthoredImageInput>,
+}
+
+#[derive(Deserialize)]
+#[serde(deny_unknown_fields)]
+struct AuthoredImageInput {
+ bytes: Vec<u8>,
+ url: String,
+ media_type: String,
+ uploaded: u64,
+ width: u32,
+ height: u32,
+ alt: String,
+ #[serde(default)]
+ fallbacks: Vec<String>,
+}
+
+#[derive(Deserialize)]
+#[serde(deny_unknown_fields)]
+struct AuthoredCommentInput {
+ content: String,
+ root: AuthoredCommentRootInput,
+ position: AuthoredCommentPositionInput,
+}
+
+#[derive(Deserialize)]
+#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
+enum AuthoredCommentRootInput {
+ Event {
+ event_id: String,
+ author: String,
+ kind: u32,
+ #[serde(default)]
+ relay: Option<String>,
+ },
+ Address {
+ coordinate: String,
+ author: String,
+ kind: u32,
+ #[serde(default)]
+ relay: Option<String>,
+ },
+}
+
+#[derive(Deserialize)]
+#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
+enum AuthoredCommentPositionInput {
+ TopEvent,
+ TopAddress { current_revision: String },
+ Nested { parent: AuthoredCommentParentInput },
+}
+
+#[derive(Deserialize)]
+#[serde(deny_unknown_fields)]
+struct AuthoredCommentParentInput {
+ event_id: String,
+ author: String,
+ #[serde(default)]
+ relay: Option<String>,
+}
+
+fn authored_post_images(
+ inputs: Vec<AuthoredImageInput>,
+) -> Result<Vec<RadrootsAuthoredPostImage>, RadrootsJsValue> {
+ inputs.into_iter().map(authored_post_image).collect()
+}
+
+fn authored_post_image(
+ input: AuthoredImageInput,
+) -> Result<RadrootsAuthoredPostImage, RadrootsJsValue> {
+ let media_type =
+ RadrootsBlossomMediaType::parse(&input.media_type).map_err(blossom_authored_error)?;
+ let sha256 = RadrootsBlossomSha256::digest(&input.bytes);
+ let size = u64::try_from(input.bytes.len())
+ .map_err(|_| authored_error("blob_size_platform_overflow"))?;
+ let descriptor = RadrootsBlossomBlobDescriptor::new(
+ RadrootsBlossomBlobUrl::parse(&input.url).map_err(blossom_authored_error)?,
+ sha256,
+ size,
+ media_type.clone(),
+ input.uploaded,
+ )
+ .map_err(blossom_authored_error)?
+ .approve_reference()
+ .map_err(blossom_authored_error)?
+ .verify_bytes(&input.bytes, &media_type)
+ .map_err(blossom_authored_error)?;
+ let image = RadrootsAuthoredImage::try_from_verified_descriptor(descriptor)
+ .map_err(image_authored_error)?;
+ let dimensions =
+ RadrootsPostImageDimensions::new(input.width, input.height).map_err(post_authored_error)?;
+ let mut authored = RadrootsAuthoredPostImage::new(image, dimensions, input.alt)
+ .map_err(post_authored_error)?;
+ for fallback in input.fallbacks {
+ let fallback = RadrootsBlossomBlobUrl::parse(&fallback)
+ .map_err(blossom_authored_error)?
+ .approve()
+ .map_err(blossom_authored_error)?;
+ authored = authored
+ .try_with_fallback(fallback)
+ .map_err(post_authored_error)?;
+ }
+ Ok(authored)
+}
+
+fn authored_comment_from_input(
+ input: AuthoredCommentInput,
+) -> Result<RadrootsAuthoredNip22Comment, RadrootsJsValue> {
+ let root = match input.root {
+ AuthoredCommentRootInput::Event {
+ event_id,
+ author,
+ kind,
+ relay,
+ } => RadrootsNip22CommentRoot::Event(
+ RadrootsNip22EventRootReference::parse(event_id, author, kind, relay.as_deref())
+ .map_err(comment_authored_error)?,
+ ),
+ AuthoredCommentRootInput::Address {
+ coordinate,
+ author,
+ kind,
+ relay,
+ } => {
+ let root = RadrootsNip22AddressRootReference::parse(coordinate, relay.as_deref())
+ .map_err(comment_authored_error)?;
+ if root.author().as_str() != author {
+ return Err(authored_error("comment_root_author_mismatch"));
+ }
+ if root.kind().as_u32() != kind {
+ return Err(authored_error("comment_root_kind_mismatch"));
+ }
+ RadrootsNip22CommentRoot::Address(root)
+ }
+ };
+
+ match (root, input.position) {
+ (RadrootsNip22CommentRoot::Event(root), AuthoredCommentPositionInput::TopEvent) => {
+ RadrootsAuthoredNip22Comment::top_level_event(input.content, root)
+ .map_err(comment_authored_error)
+ }
+ (
+ RadrootsNip22CommentRoot::Address(root),
+ AuthoredCommentPositionInput::TopAddress { current_revision },
+ ) => RadrootsAuthoredNip22Comment::parse_top_level_address(
+ input.content,
+ root,
+ current_revision,
+ )
+ .map_err(comment_authored_error),
+ (root, AuthoredCommentPositionInput::Nested { parent }) => {
+ let parent = RadrootsNip22CommentParentReference::parse(
+ parent.event_id,
+ parent.author,
+ parent.relay.as_deref(),
+ )
+ .map_err(comment_authored_error)?;
+ RadrootsAuthoredNip22Comment::nested(input.content, root, parent)
+ .map_err(comment_authored_error)
+ }
+ _ => Err(authored_error("comment_root_position_incompatible")),
+ }
+}
+
+/// Builds deterministic unsigned kind-1 wire parts for a strict Update.
+#[cfg_attr(
+ target_arch = "wasm32",
+ wasm_bindgen(js_name = social_update_build_authored_draft)
+)]
+pub fn social_update_build_authored_draft(input_json: &str) -> Result<String, RadrootsJsValue> {
+ let input = parse_authored_json::<AuthoredUpdateInput>(input_json)?;
+ let update = RadrootsAuthoredUpdate::new(input.content).map_err(post_authored_error)?;
+ wire_parts_to_json(&authored_update_to_wire_parts(&update))
+}
+
+/// Builds deterministic unsigned kind-1 wire parts for a strict PhotoUpdate.
+#[cfg_attr(
+ target_arch = "wasm32",
+ wasm_bindgen(js_name = social_photo_update_build_authored_draft)
+)]
+pub fn social_photo_update_build_authored_draft(
+ input_json: &str,
+) -> Result<String, RadrootsJsValue> {
+ let input = parse_authored_json::<AuthoredMediaPostInput>(input_json)?;
+ let images = authored_post_images(input.images)?;
+ let photo =
+ RadrootsAuthoredPhotoUpdate::new(input.content, images).map_err(post_authored_error)?;
+ wire_parts_to_json(&authored_photo_update_to_wire_parts(&photo))
+}
+
+/// Builds deterministic unsigned kind-1 wire parts for a strict Ask.
+#[cfg_attr(
+ target_arch = "wasm32",
+ wasm_bindgen(js_name = social_ask_build_authored_draft)
+)]
+pub fn social_ask_build_authored_draft(input_json: &str) -> Result<String, RadrootsJsValue> {
+ let input = parse_authored_json::<AuthoredMediaPostInput>(input_json)?;
+ let images = authored_post_images(input.images)?;
+ let ask = RadrootsAuthoredAsk::new(input.content, images).map_err(post_authored_error)?;
+ wire_parts_to_json(&authored_ask_to_wire_parts(&ask))
}
-#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = listing_tags_full))]
-pub fn listing_tags_full(listing_json: &str) -> Result<String, RadrootsJsValue> {
- build_tags_json::<RadrootsListing, _, _>(listing_json, listing_tags_full_impl)
+/// Builds deterministic unsigned kind-1111 wire parts for a strict NIP-22 Comment.
+#[cfg_attr(
+ target_arch = "wasm32",
+ wasm_bindgen(js_name = social_comment_build_authored_draft)
+)]
+pub fn social_comment_build_authored_draft(input_json: &str) -> Result<String, RadrootsJsValue> {
+ let input = parse_authored_json::<AuthoredCommentInput>(input_json)?;
+ let comment = authored_comment_from_input(input)?;
+ wire_parts_to_json(&authored_nip22_comment_to_wire_parts(&comment))
}
-#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = post_tags))]
-pub fn post_tags(post_json: &str) -> Result<String, RadrootsJsValue> {
- build_tags_json::<RadrootsPost, _, _>(post_json, post_build_tags)
+#[cfg_attr(
+ target_arch = "wasm32",
+ wasm_bindgen(js_name = operational_listing_tags)
+)]
+pub fn operational_listing_tags(listing_json: &str) -> Result<String, RadrootsJsValue> {
+ build_tags_json::<RadrootsOperationalListing, _, _>(listing_json, operational_listing_tags_impl)
}
-#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = comment_tags))]
-pub fn comment_tags(comment_json: &str) -> Result<String, RadrootsJsValue> {
- build_tags_json::<RadrootsComment, _, _>(comment_json, comment_build_tags)
+#[cfg_attr(
+ target_arch = "wasm32",
+ wasm_bindgen(js_name = operational_listing_tags_full)
+)]
+pub fn operational_listing_tags_full(listing_json: &str) -> Result<String, RadrootsJsValue> {
+ build_tags_json::<RadrootsOperationalListing, _, _>(
+ listing_json,
+ operational_listing_tags_full_impl,
+ )
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen(js_name = article_tags))]
@@ -606,18 +874,19 @@ mod tests {
RadrootsKnowledgeReviewScope, RadrootsKnowledgeReviewScore, RadrootsKnowledgeReviewTarget,
RadrootsWikiArticleVersionRef,
};
- use radroots_event::listing::{RadrootsListingBin, RadrootsListingProduct};
+ use radroots_event::operational_listing::{
+ RadrootsOperationalListingBin, RadrootsOperationalListingProduct,
+ };
use radroots_event::relay_auth::RadrootsRelayAuth;
use radroots_event::social::{
RadrootsReportFileTarget, RadrootsReportType, RadrootsSocialFarmAnchor,
- RadrootsSocialLocation, RadrootsSocialMediaDimensions, RadrootsSocialMediaMetadata,
- RadrootsSocialTarget,
+ RadrootsSocialLocation, RadrootsSocialMediaDimensions, RadrootsSocialTarget,
};
use radroots_event::{
RadrootsEventEnvelope, RadrootsEventEnvelopeLimits, RadrootsEventEnvelopeParts,
};
- fn sample_listing() -> RadrootsListing {
+ fn sample_listing() -> RadrootsOperationalListing {
let quantity =
RadrootsCoreQuantity::new(RadrootsCoreDecimal::from(1u32), RadrootsCoreUnit::Each);
let price = RadrootsCoreQuantityPrice::new(
@@ -625,14 +894,14 @@ mod tests {
quantity.clone(),
);
- RadrootsListing {
+ RadrootsOperationalListing {
d_tag: "AAAAAAAAAAAAAAAAAAAAAg".parse().expect("listing d tag"),
published_at: None,
farm: RadrootsFarmRef {
pubkey: "farm_pubkey".to_string(),
d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_string(),
},
- product: RadrootsListingProduct {
+ product: RadrootsOperationalListingProduct {
key: "sku".to_string(),
title: "widget".to_string(),
category: "tools".to_string(),
@@ -644,7 +913,7 @@ mod tests {
year: None,
},
primary_bin_id: "bin-1".parse().expect("primary bin id"),
- bins: vec![RadrootsListingBin {
+ bins: vec![RadrootsOperationalListingBin {
bin_id: "bin-1".parse().expect("bin id"),
quantity,
price_per_canonical_unit: price,
@@ -888,9 +1157,7 @@ mod tests {
.map(nostr::Tag::parse)
.collect::<Result<Vec<_>, _>>()
.expect("parsed tags");
- let keys =
- nostr::Keys::parse("0101010101010101010101010101010101010101010101010101010101010101")
- .expect("keys");
+ let keys = nostr::Keys::generate();
let event =
nostr::EventBuilder::new(nostr::Kind::Custom(KIND_KNOWLEDGE_CLAIM as u16), claim_json)
.tags(tags)
@@ -922,40 +1189,6 @@ mod tests {
}
}
- fn sample_post() -> RadrootsPost {
- RadrootsPost {
- content: "field update".to_string(),
- farm: Some(social_farm_anchor()),
- address_refs: Some(vec![address_target(30023, "AAAAAAAAAAAAAAAAAAAAAQ")]),
- location: Some(social_location()),
- topics: Some(vec!["soil".to_string(), "market".to_string()]),
- quote_refs: Some(vec![event_target(30023, 'd')]),
- media: Some(vec![RadrootsSocialMediaMetadata {
- url: Some("https://media.example.test/field.jpg".to_string()),
- mime_type: Some("image/jpeg".to_string()),
- sha256: Some(
- "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(),
- ),
- original_sha256: None,
- size: Some(4096),
- dimensions: Some(RadrootsSocialMediaDimensions {
- width: 1200,
- height: 800,
- }),
- blurhash: None,
- thumbnails: None,
- image: None,
- summary: Some("field photo".to_string()),
- alt: Some("rows after harvest".to_string()),
- fallback: None,
- magnet: Some("magnet:?xt=urn:btih:abc".to_string()),
- content_hashes: Some(vec!["sha256:field".to_string()]),
- services: Some(vec!["https://media.example.test".to_string()]),
- imeta: None,
- }]),
- }
- }
-
fn sample_article() -> RadrootsArticle {
RadrootsArticle {
d_tag: "AAAAAAAAAAAAAAAAAAAAAg".to_string(),
@@ -995,14 +1228,6 @@ mod tests {
}
}
- fn sample_comment() -> RadrootsComment {
- RadrootsComment {
- root: event_target(30023, 'a'),
- parent: address_target(30023, "AAAAAAAAAAAAAAAAAAAAAg"),
- content: "great notes".to_string(),
- }
- }
-
fn sample_reaction() -> RadrootsReaction {
RadrootsReaction {
target: address_target(30023, "AAAAAAAAAAAAAAAAAAAAAg"),
@@ -1196,13 +1421,136 @@ mod tests {
type BindingEncoder = fn(&str) -> Result<String, RadrootsJsValue>;
+ const POST_AUTHORED_VECTORS: &str = include_str!(
+ "../../../../lib/contracts/conformance/vectors/post/verified_profiles.v1.json"
+ );
+ const COMMENT_AUTHORED_VECTORS: &str = include_str!(
+ "../../../../lib/contracts/conformance/vectors/comment/verified_profile.v1.json"
+ );
+
+ #[derive(Deserialize)]
+ #[serde(deny_unknown_fields)]
+ struct AuthoredVectorSuite {
+ suite: String,
+ contract_version: String,
+ vectors: Vec<AuthoredVector>,
+ }
+
+ #[derive(Deserialize)]
+ #[serde(deny_unknown_fields)]
+ struct AuthoredVector {
+ id: String,
+ kind: String,
+ input: serde_json::Value,
+ expected: serde_json::Value,
+ }
+
+ fn authored_binding(kind: &str) -> Option<BindingEncoder> {
+ if kind.starts_with("social.update.build_authored_draft.") {
+ Some(social_update_build_authored_draft)
+ } else if kind.starts_with("social.photo_update.build_authored_draft.") {
+ Some(social_photo_update_build_authored_draft)
+ } else if kind.starts_with("social.ask.build_authored_draft.") {
+ Some(social_ask_build_authored_draft)
+ } else if kind.starts_with("social.comment.build_authored_draft.") {
+ Some(social_comment_build_authored_draft)
+ } else {
+ None
+ }
+ }
+
+ fn authored_vector_request(vector: &AuthoredVector) -> String {
+ let mut input = vector.input.clone();
+ if let Some(images) = input
+ .get_mut("images")
+ .and_then(serde_json::Value::as_array_mut)
+ {
+ for image in images {
+ let image = image
+ .as_object_mut()
+ .unwrap_or_else(|| panic!("{} image must be an object", vector.id));
+ let bytes_utf8 = image
+ .remove("bytes_utf8")
+ .unwrap_or_else(|| panic!("{} image must carry vector bytes", vector.id));
+ let bytes_utf8 = bytes_utf8
+ .as_str()
+ .unwrap_or_else(|| panic!("{} vector bytes must be UTF-8 text", vector.id));
+ image.insert(
+ "bytes".to_owned(),
+ serde_json::Value::Array(
+ bytes_utf8
+ .as_bytes()
+ .iter()
+ .copied()
+ .map(serde_json::Value::from)
+ .collect(),
+ ),
+ );
+ }
+ }
+ serde_json::to_string(&input)
+ .unwrap_or_else(|error| panic!("{} request JSON failed: {error}", vector.id))
+ }
+
+ fn authored_error_value(error: RadrootsJsValue) -> serde_json::Value {
+ serde_json::from_str(&error).expect("authored binding error must be coded JSON")
+ }
+
+ fn authored_error_inner(error: RadrootsJsValue) -> String {
+ let error = authored_error_value(error);
+ assert_eq!(error["code"], "encode_error");
+ error["inner_code"]
+ .as_str()
+ .expect("authored error inner_code")
+ .to_owned()
+ }
+
+ fn execute_authored_vectors(raw: &str, expected_suite: &str, expected_vector_count: usize) {
+ let suite: AuthoredVectorSuite =
+ serde_json::from_str(raw).expect("owner conformance vectors");
+ assert_eq!(suite.suite, expected_suite);
+ assert_eq!(suite.contract_version, "1.0.0");
+ let mut executed = 0usize;
+ for vector in suite.vectors {
+ let Some(binding) = authored_binding(&vector.kind) else {
+ continue;
+ };
+ executed += 1;
+ let request = authored_vector_request(&vector);
+ if vector.kind.ends_with(".valid") {
+ let first = binding(&request)
+ .unwrap_or_else(|error| panic!("{} failed: {error}", vector.id));
+ let second = binding(&request)
+ .unwrap_or_else(|error| panic!("{} repeat failed: {error}", vector.id));
+ assert_eq!(first, second, "{} repeat encoding drifted", vector.id);
+ let actual: serde_json::Value = serde_json::from_str(&first)
+ .unwrap_or_else(|error| panic!("{} output JSON failed: {error}", vector.id));
+ assert_eq!(actual, vector.expected, "{}", vector.id);
+ } else if vector.kind.ends_with(".invalid") {
+ let first = binding(&request).expect_err("invalid authored vector must fail");
+ let second =
+ binding(&request).expect_err("repeated invalid authored vector must fail");
+ let expected = vector.expected["error"]
+ .as_str()
+ .unwrap_or_else(|| panic!("{} expected error must be a string", vector.id));
+ assert_eq!(authored_error_inner(first), expected, "{}", vector.id);
+ assert_eq!(authored_error_inner(second), expected, "{}", vector.id);
+ } else {
+ panic!("{} has unsupported authored vector kind", vector.id);
+ }
+ }
+ assert_eq!(executed, expected_vector_count);
+ }
+
#[test]
fn bindings_reject_invalid_json() {
- let bindings: [BindingEncoder; 42] = [
- listing_tags,
- listing_tags_full,
- post_tags,
- comment_tags,
+ let bindings: [BindingEncoder; 44] = [
+ social_update_build_authored_draft,
+ social_photo_update_build_authored_draft,
+ social_ask_build_authored_draft,
+ social_comment_build_authored_draft,
+ operational_listing_tags,
+ operational_listing_tags_full,
article_tags,
file_metadata_tags,
repost_tags,
@@ -1246,13 +1594,107 @@ mod tests {
for binding in bindings {
assert!(binding("{").is_err());
}
- assert!(listing_tags("").is_err());
+ assert!(operational_listing_tags("").is_err());
+ }
+
+ #[test]
+ fn strict_authored_post_bindings_execute_owner_conformance_vectors() {
+ execute_authored_vectors(POST_AUTHORED_VECTORS, "post_profiles", 6);
+ }
+
+ #[test]
+ fn strict_authored_comment_binding_executes_owner_conformance_vectors() {
+ execute_authored_vectors(COMMENT_AUTHORED_VECTORS, "nip22_comment_profile", 31);
+ }
+
+ #[test]
+ fn authored_media_binding_verifies_exact_arbitrary_bytes() {
+ let bytes = vec![0, 159, 146, 150];
+ let hash = RadrootsBlossomSha256::digest(&bytes);
+ let url = format!("https://media.example/{hash}.webp");
+ let input = serde_json::json!({
+ "content": format!("Harvest {url}"),
+ "images": [{
+ "bytes": bytes,
+ "url": url,
+ "media_type": "image/webp",
+ "uploaded": 1_784_347_200_u64,
+ "width": 1200,
+ "height": 900,
+ "alt": "Harvest",
+ "fallbacks": [],
+ }],
+ });
+
+ let encoded = social_photo_update_build_authored_draft(
+ &serde_json::to_string(&input).expect("media request"),
+ )
+ .expect("byte-verified PhotoUpdate");
+ let encoded: RadrootsNip01EventWireParts =
+ serde_json::from_str(&encoded).expect("wire parts");
+ assert_eq!(encoded.tags.len(), 1);
+ assert!(encoded.tags[0].contains(&format!("x {hash}")));
+ assert!(encoded.tags[0].contains(&"size 4".to_owned()));
+
+ let mut wrong_bytes = input.clone();
+ wrong_bytes["images"][0]["bytes"] = serde_json::json!([1, 2, 3, 4]);
+ let error = social_photo_update_build_authored_draft(
+ &serde_json::to_string(&wrong_bytes).expect("wrong-byte request"),
+ )
+ .expect_err("URL digest must bind exact bytes");
+ assert_eq!(authored_error_inner(error), "descriptor_hash_mismatch");
+ }
+
+ #[test]
+ fn authored_media_binding_rejects_fabricated_descriptor_fields() {
+ let bytes = b"image".to_vec();
+ let hash = RadrootsBlossomSha256::digest(&bytes);
+ let url = format!("https://media.example/{hash}.webp");
+ let mut input = serde_json::json!({
+ "content": format!("Harvest {url}"),
+ "images": [{
+ "bytes": bytes,
+ "url": url,
+ "media_type": "image/webp",
+ "uploaded": 1_784_347_200_u64,
+ "width": 1200,
+ "height": 900,
+ "alt": "Harvest",
+ }],
+ });
+ input["images"][0]["sha256"] = serde_json::json!(hash.to_string());
+ input["images"][0]["size"] = serde_json::json!(5);
+
+ let error = social_photo_update_build_authored_draft(
+ &serde_json::to_string(&input).expect("fabricated descriptor request"),
+ )
+ .expect_err("caller-supplied descriptor commitments must be rejected");
+ let error = authored_error_value(error);
+ assert_eq!(error["code"], "invalid_json");
+ assert_eq!(error["inner_code"], "authored_input");
+ }
+
+ #[test]
+ fn ask_allows_zero_media_while_photo_update_requires_media() {
+ let ask = social_ask_build_authored_draft(r#"{"content":"When is harvest?","images":[]}"#)
+ .expect("Ask without media");
+ let ask: RadrootsNip01EventWireParts = serde_json::from_str(&ask).expect("Ask wire parts");
+ assert_eq!(
+ ask.tags,
+ vec![vec!["t".to_owned(), "radroots-ask".to_owned()]]
+ );
+
+ let error =
+ social_photo_update_build_authored_draft(r#"{"content":"Harvest","images":[]}"#)
+ .expect_err("PhotoUpdate without media");
+ assert_eq!(authored_error_inner(error), "photo_imeta_missing");
}
#[test]
fn bindings_encode_to_json_when_input_is_valid() {
let listing_json = serde_json::to_string(&sample_listing()).expect("listing json");
- let listing_tags_json = listing_tags(&listing_json).expect("listing tags");
+ let listing_tags_json =
+ operational_listing_tags(&listing_json).expect("operational listing tags");
let listing_tags: Vec<Vec<String>> =
serde_json::from_str(&listing_tags_json).expect("listing tags json");
assert!(!listing_tags.is_empty());
@@ -1266,12 +1708,6 @@ mod tests {
#[test]
fn social_bindings_encode_to_json_when_input_is_valid() {
- assert_tags_json(post_tags(
- &serde_json::to_string(&sample_post()).expect("post json"),
- ));
- assert_tags_json(comment_tags(
- &serde_json::to_string(&sample_comment()).expect("comment json"),
- ));
assert_tags_json(article_tags(
&serde_json::to_string(&sample_article()).expect("article json"),
));
@@ -1298,10 +1734,6 @@ mod tests {
article.d_tag.clear();
assert!(article_tags(&serde_json::to_string(&article).expect("article json")).is_err());
- let mut comment = sample_comment();
- comment.root = event_target(1, 'a');
- assert!(comment_tags(&serde_json::to_string(&comment).expect("comment json")).is_err());
-
let mut reaction = sample_reaction();
reaction.target = RadrootsSocialTarget::External {
id: "https://example.test/object".to_string(),
@@ -1687,7 +2119,7 @@ mod tests {
listing_json["bins"] = serde_json::Value::Array(Vec::new());
let listing_json = serde_json::to_string(&listing_json).expect("listing json");
- assert!(listing_tags(&listing_json).is_err());
- assert!(listing_tags_full(&listing_json).is_err());
+ assert!(operational_listing_tags(&listing_json).is_err());
+ assert!(operational_listing_tags_full(&listing_json).is_err());
}
}
diff --git a/crates/replica_sync_wasm/src/lib.rs b/crates/replica_sync_wasm/src/lib.rs
@@ -5,11 +5,12 @@ use base64::Engine;
#[cfg(target_arch = "wasm32")]
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use radroots_event::{RadrootsEventEnvelope, RadrootsEventEnvelopeParts};
+#[cfg(any(target_arch = "wasm32", test))]
+use radroots_replica_sync::RadrootsReplicaIngestOutcome;
use radroots_replica_sync::RadrootsReplicaSyncRequest;
#[cfg(target_arch = "wasm32")]
use radroots_replica_sync::{
- RadrootsReplicaIdFactory, RadrootsReplicaIngestOutcome,
- radroots_replica_ingest_event_with_factory, radroots_replica_sync_all,
+ RadrootsReplicaIdFactory, radroots_replica_ingest_event_with_factory, radroots_replica_sync_all,
};
#[cfg(target_arch = "wasm32")]
use radroots_sdk_sql_wasm_runtime::WasmSqlExecutor;
@@ -66,6 +67,16 @@ pub fn parse_event_model(event_json: &str) -> Result<RadrootsEventEnvelope, Stri
.map_err(|error| error.to_string())
}
+#[cfg(any(target_arch = "wasm32", test))]
+fn ingest_outcome_label(outcome: RadrootsReplicaIngestOutcome) -> &'static str {
+ match outcome {
+ RadrootsReplicaIngestOutcome::Applied => "applied",
+ RadrootsReplicaIngestOutcome::Excluded => "excluded",
+ RadrootsReplicaIngestOutcome::Rejected => "rejected",
+ RadrootsReplicaIngestOutcome::Skipped => "skipped",
+ }
+}
+
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen(js_name = replica_sync_sync_all)]
pub fn replica_sync_sync_all(request_json: &str) -> Result<JsValue, JsValue> {
@@ -83,16 +94,13 @@ pub fn replica_sync_ingest_event(event_json: &str) -> Result<JsValue, JsValue> {
let factory = WasmIdFactory;
let outcome =
radroots_replica_ingest_event_with_factory(&exec, &event, &factory).map_err(err_js)?;
- let value = match outcome {
- RadrootsReplicaIngestOutcome::Applied => "applied",
- RadrootsReplicaIngestOutcome::Skipped => "skipped",
- };
- Ok(JsValue::from_str(value))
+ Ok(JsValue::from_str(ingest_outcome_label(outcome)))
}
#[cfg(test)]
mod tests {
- use super::{parse_event_model, parse_request_model};
+ use super::{ingest_outcome_label, parse_event_model, parse_request_model};
+ use radroots_replica_sync::RadrootsReplicaIngestOutcome;
fn event_json(author: Option<&str>, pubkey: Option<&str>) -> String {
let mut fields = vec![
@@ -148,4 +156,24 @@ mod tests {
let error = parse_request_model("{").expect_err("error");
assert!(error.contains("EOF"));
}
+
+ #[test]
+ fn ingest_outcome_labels_cover_the_replica_contract() {
+ assert_eq!(
+ ingest_outcome_label(RadrootsReplicaIngestOutcome::Applied),
+ "applied"
+ );
+ assert_eq!(
+ ingest_outcome_label(RadrootsReplicaIngestOutcome::Excluded),
+ "excluded"
+ );
+ assert_eq!(
+ ingest_outcome_label(RadrootsReplicaIngestOutcome::Rejected),
+ "rejected"
+ );
+ assert_eq!(
+ ingest_outcome_label(RadrootsReplicaIngestOutcome::Skipped),
+ "skipped"
+ );
+ }
}
diff --git a/crates/sdk/examples/runtime_local.rs b/crates/sdk/examples/runtime_local.rs
@@ -6,7 +6,12 @@ use radroots_core::{
use radroots_event::contract::RadrootsActorRole;
use radroots_event::farm::RadrootsFarmRef;
use radroots_event::ids::{RadrootsDTag, RadrootsInventoryBinId};
-use radroots_event::listing::{RadrootsListing, RadrootsListingBin, RadrootsListingProduct};
+use radroots_event::operational_listing::{
+ RadrootsOperationalListing, RadrootsOperationalListingAvailability,
+ RadrootsOperationalListingBin, RadrootsOperationalListingDeliveryMethod,
+ RadrootsOperationalListingProduct, RadrootsOperationalListingPublicLocation,
+ RadrootsOperationalListingStatus,
+};
use radroots_nostr::prelude::RadrootsNostrKeys;
use radroots_sdk::{
ListingPreparePublishRequest, NostrRelayUrlPolicy, PushOutboxRequest, RadrootsClient,
@@ -48,7 +53,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.push_outbox(PushOutboxRequest::new().with_limit(1))
.await;
assert_eq!(
- prepared.public_listing_addr.as_str(),
+ prepared.public_listing_addr().as_str(),
enqueue.public_listing_addr.as_str()
);
#[cfg(feature = "transport-nostr-runtime")]
@@ -64,15 +69,15 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
Ok(())
}
-fn sample_listing(seller: &str) -> RadrootsListing {
- RadrootsListing {
+fn sample_listing(seller: &str) -> RadrootsOperationalListing {
+ RadrootsOperationalListing {
d_tag: RadrootsDTag::parse("AAAAAAAAAAAAAAAAAAAAAQ").expect("d tag"),
published_at: None,
farm: RadrootsFarmRef {
pubkey: seller.to_owned(),
d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_owned(),
},
- product: RadrootsListingProduct {
+ product: RadrootsOperationalListingProduct {
key: "coffee".to_owned(),
title: "Coffee".to_owned(),
category: "coffee".to_owned(),
@@ -84,7 +89,7 @@ fn sample_listing(seller: &str) -> RadrootsListing {
year: None,
},
primary_bin_id: RadrootsInventoryBinId::parse("bin-1").expect("bin id"),
- bins: vec![RadrootsListingBin {
+ bins: vec![RadrootsOperationalListingBin {
bin_id: RadrootsInventoryBinId::parse("bin-1").expect("bin id"),
quantity: RadrootsCoreQuantity::new(
RadrootsCoreDecimal::from(1000u32),
@@ -109,10 +114,18 @@ fn sample_listing(seller: &str) -> RadrootsListing {
resource_area: None,
plot: None,
discounts: None,
- inventory_available: None,
- availability: None,
- delivery_method: None,
- location: None,
+ inventory_available: Some(RadrootsCoreDecimal::from(5u32)),
+ availability: Some(RadrootsOperationalListingAvailability::Status {
+ status: RadrootsOperationalListingStatus::Active,
+ }),
+ delivery_method: Some(RadrootsOperationalListingDeliveryMethod::Pickup),
+ location: Some(RadrootsOperationalListingPublicLocation {
+ primary: "Victoria".to_owned(),
+ city: Some("Victoria".to_owned()),
+ region: Some("British Columbia".to_owned()),
+ country: Some("CA".to_owned()),
+ geohash: "c287g".to_owned(),
+ }),
images: None,
}
}
diff --git a/crates/sdk/examples/sdk_v1_knowledge_prepare.rs b/crates/sdk/examples/sdk_v1_knowledge_prepare.rs
@@ -2,15 +2,15 @@ use nostr::{EventBuilder, Keys, Kind, Tag, Timestamp};
use radroots_event::RadrootsEventEnvelopeParts;
use radroots_sdk::knowledge::prelude::*;
-const SECRET_KEY_HEX: &str = "0101010101010101010101010101010101010101010101010101010101010101";
const CREATED_AT: u32 = 1_800_000_000;
const RELAY: &str = "wss://relay.radroots.example";
fn main() -> Result<(), Box<dyn std::error::Error>> {
+ let keys = Keys::generate();
let claim = claim_builder().build()?;
let parts = claim_builder().build_event()?;
- let draft = claim_builder().build_draft(public_key_hex(), CREATED_AT)?;
- let signed = sign_parts(parts)?;
+ let draft = claim_builder().build_draft(keys.public_key().to_hex(), CREATED_AT)?;
+ let signed = sign_parts(parts, &keys)?;
let decoded = KnowledgeCodec::new().verify_and_decode_radroots_event(signed)?;
let manifest = contract_manifest();
let manifest_hash = contract_manifest_sha256()?;
@@ -36,17 +36,17 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
fn sign_parts(
parts: RadrootsNip01EventWireParts,
+ keys: &Keys,
) -> Result<RadrootsEventEnvelope, Box<dyn std::error::Error>> {
let tags = parts
.tags
.into_iter()
.map(Tag::parse)
.collect::<Result<Vec<_>, _>>()?;
- let keys = Keys::parse(SECRET_KEY_HEX)?;
let event = EventBuilder::new(Kind::Custom(parts.kind as u16), parts.content)
.tags(tags)
.custom_created_at(Timestamp::from_secs(u64::from(CREATED_AT)))
- .sign_with_keys(&keys)?;
+ .sign_with_keys(keys)?;
Ok(RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts {
id: event.id.to_hex(),
author: event.pubkey.to_hex(),
@@ -63,13 +63,6 @@ fn sign_parts(
})?)
}
-fn public_key_hex() -> String {
- Keys::parse(SECRET_KEY_HEX)
- .expect("keys")
- .public_key()
- .to_hex()
-}
-
fn hex_64(character: char) -> String {
character.to_string().repeat(64)
}
diff --git a/crates/sdk/examples/sdk_v1_listing_prepare.rs b/crates/sdk/examples/sdk_v1_listing_prepare.rs
@@ -6,7 +6,12 @@ use radroots_core::{
use radroots_event::contract::RadrootsActorRole;
use radroots_event::farm::RadrootsFarmRef;
use radroots_event::ids::{RadrootsDTag, RadrootsInventoryBinId};
-use radroots_event::listing::{RadrootsListing, RadrootsListingBin, RadrootsListingProduct};
+use radroots_event::operational_listing::{
+ RadrootsOperationalListing, RadrootsOperationalListingAvailability,
+ RadrootsOperationalListingBin, RadrootsOperationalListingDeliveryMethod,
+ RadrootsOperationalListingProduct, RadrootsOperationalListingPublicLocation,
+ RadrootsOperationalListingStatus,
+};
use radroots_sdk::{ListingPreparePublishRequest, RadrootsClient, RadrootsSdkTimestamp};
const SELLER: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
@@ -22,20 +27,20 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let plan = sdk.listings().prepare_publish(request)?;
- println!("public listing: {}", plan.public_listing_addr.as_str());
- println!("expected event: {}", plan.expected_event_id.as_str());
+ println!("public listing: {}", plan.public_listing_addr().as_str());
+ println!("expected event: {}", plan.expected_event_id().as_str());
Ok(())
}
-fn sample_listing(seller: &str) -> RadrootsListing {
- RadrootsListing {
+fn sample_listing(seller: &str) -> RadrootsOperationalListing {
+ RadrootsOperationalListing {
d_tag: RadrootsDTag::parse("AAAAAAAAAAAAAAAAAAAAAQ").expect("d tag"),
published_at: None,
farm: RadrootsFarmRef {
pubkey: seller.to_owned(),
d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_owned(),
},
- product: RadrootsListingProduct {
+ product: RadrootsOperationalListingProduct {
key: "coffee".to_owned(),
title: "Coffee".to_owned(),
category: "coffee".to_owned(),
@@ -47,7 +52,7 @@ fn sample_listing(seller: &str) -> RadrootsListing {
year: None,
},
primary_bin_id: RadrootsInventoryBinId::parse("bin-1").expect("bin id"),
- bins: vec![RadrootsListingBin {
+ bins: vec![RadrootsOperationalListingBin {
bin_id: RadrootsInventoryBinId::parse("bin-1").expect("bin id"),
quantity: RadrootsCoreQuantity::new(
RadrootsCoreDecimal::from(1000u32),
@@ -72,10 +77,18 @@ fn sample_listing(seller: &str) -> RadrootsListing {
resource_area: None,
plot: None,
discounts: None,
- inventory_available: None,
- availability: None,
- delivery_method: None,
- location: None,
+ inventory_available: Some(RadrootsCoreDecimal::from(5u32)),
+ availability: Some(RadrootsOperationalListingAvailability::Status {
+ status: RadrootsOperationalListingStatus::Active,
+ }),
+ delivery_method: Some(RadrootsOperationalListingDeliveryMethod::Pickup),
+ location: Some(RadrootsOperationalListingPublicLocation {
+ primary: "Victoria".to_owned(),
+ city: Some("Victoria".to_owned()),
+ region: Some("British Columbia".to_owned()),
+ country: Some("CA".to_owned()),
+ geohash: "c287g".to_owned(),
+ }),
images: None,
}
}
diff --git a/crates/sdk/examples/sdk_v1_local_enqueue_and_mock_sync.rs b/crates/sdk/examples/sdk_v1_local_enqueue_and_mock_sync.rs
@@ -6,7 +6,12 @@ use radroots_core::{
use radroots_event::contract::RadrootsActorRole;
use radroots_event::farm::RadrootsFarmRef;
use radroots_event::ids::{RadrootsDTag, RadrootsInventoryBinId};
-use radroots_event::listing::{RadrootsListing, RadrootsListingBin, RadrootsListingProduct};
+use radroots_event::operational_listing::{
+ RadrootsOperationalListing, RadrootsOperationalListingAvailability,
+ RadrootsOperationalListingBin, RadrootsOperationalListingDeliveryMethod,
+ RadrootsOperationalListingProduct, RadrootsOperationalListingPublicLocation,
+ RadrootsOperationalListingStatus,
+};
use radroots_nostr::prelude::RadrootsNostrKeys;
use radroots_sdk::{
ListingPreparePublishRequest, NostrRelayUrlPolicy, PushOutboxRequest, RadrootsClient,
@@ -61,15 +66,15 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
Ok(())
}
-fn sample_listing(seller: &str) -> RadrootsListing {
- RadrootsListing {
+fn sample_listing(seller: &str) -> RadrootsOperationalListing {
+ RadrootsOperationalListing {
d_tag: RadrootsDTag::parse("AAAAAAAAAAAAAAAAAAAAAQ").expect("d tag"),
published_at: None,
farm: RadrootsFarmRef {
pubkey: seller.to_owned(),
d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_owned(),
},
- product: RadrootsListingProduct {
+ product: RadrootsOperationalListingProduct {
key: "coffee".to_owned(),
title: "Coffee".to_owned(),
category: "coffee".to_owned(),
@@ -81,7 +86,7 @@ fn sample_listing(seller: &str) -> RadrootsListing {
year: None,
},
primary_bin_id: RadrootsInventoryBinId::parse("bin-1").expect("bin id"),
- bins: vec![RadrootsListingBin {
+ bins: vec![RadrootsOperationalListingBin {
bin_id: RadrootsInventoryBinId::parse("bin-1").expect("bin id"),
quantity: RadrootsCoreQuantity::new(
RadrootsCoreDecimal::from(1000u32),
@@ -106,10 +111,18 @@ fn sample_listing(seller: &str) -> RadrootsListing {
resource_area: None,
plot: None,
discounts: None,
- inventory_available: None,
- availability: None,
- delivery_method: None,
- location: None,
+ inventory_available: Some(RadrootsCoreDecimal::from(5u32)),
+ availability: Some(RadrootsOperationalListingAvailability::Status {
+ status: RadrootsOperationalListingStatus::Active,
+ }),
+ delivery_method: Some(RadrootsOperationalListingDeliveryMethod::Pickup),
+ location: Some(RadrootsOperationalListingPublicLocation {
+ primary: "Victoria".to_owned(),
+ city: Some("Victoria".to_owned()),
+ region: Some("British Columbia".to_owned()),
+ country: Some("CA".to_owned()),
+ geohash: "c287g".to_owned(),
+ }),
images: None,
}
}
diff --git a/crates/sdk/src/error.rs b/crates/sdk/src/error.rs
@@ -6,6 +6,8 @@ use crate::privacy::{PrivacyPreflightStatus, ProductSensitivityField};
#[cfg(feature = "runtime")]
use crate::transport::ReticulumBehavior;
#[cfg(feature = "runtime")]
+use radroots_event::trade_validation::RadrootsOperationalListingValidationError;
+#[cfg(feature = "runtime")]
use serde_json::{Value, json};
#[cfg(feature = "runtime")]
@@ -58,6 +60,91 @@ pub enum RadrootsSdkGeoNamesErrorKind {
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
+pub enum RadrootsSdkListingValidationErrorKind {
+ InvalidKind,
+ InvalidProfile,
+ MissingListingId,
+ ListingEventNotFound,
+ ListingEventFetchFailed,
+ ParseError,
+ InvalidSeller,
+ MissingFarmProfile,
+ MissingFarmRecord,
+ MissingTitle,
+ MissingDescription,
+ MissingProductType,
+ MissingBins,
+ MissingPrimaryBin,
+ InvalidBin,
+ MissingPrice,
+ InvalidPrice,
+ MissingInventory,
+ InvalidInventory,
+ MissingAvailability,
+ MissingLocation,
+ MissingLocationLocality,
+ MissingLocationGeohash,
+ InvalidLocationGeohash,
+ MissingDeliveryMethod,
+}
+
+#[cfg(feature = "runtime")]
+impl From<&RadrootsOperationalListingValidationError> for RadrootsSdkListingValidationErrorKind {
+ fn from(error: &RadrootsOperationalListingValidationError) -> Self {
+ match error {
+ RadrootsOperationalListingValidationError::InvalidKind { .. } => Self::InvalidKind,
+ RadrootsOperationalListingValidationError::InvalidProfile => Self::InvalidProfile,
+ RadrootsOperationalListingValidationError::MissingListingId => Self::MissingListingId,
+ RadrootsOperationalListingValidationError::ListingEventNotFound { .. } => {
+ Self::ListingEventNotFound
+ }
+ RadrootsOperationalListingValidationError::ListingEventFetchFailed { .. } => {
+ Self::ListingEventFetchFailed
+ }
+ RadrootsOperationalListingValidationError::ParseError { .. } => Self::ParseError,
+ RadrootsOperationalListingValidationError::InvalidSeller => Self::InvalidSeller,
+ RadrootsOperationalListingValidationError::MissingFarmProfile => {
+ Self::MissingFarmProfile
+ }
+ RadrootsOperationalListingValidationError::MissingFarmRecord => Self::MissingFarmRecord,
+ RadrootsOperationalListingValidationError::MissingTitle => Self::MissingTitle,
+ RadrootsOperationalListingValidationError::MissingDescription => {
+ Self::MissingDescription
+ }
+ RadrootsOperationalListingValidationError::MissingProductType => {
+ Self::MissingProductType
+ }
+ RadrootsOperationalListingValidationError::MissingBins => Self::MissingBins,
+ RadrootsOperationalListingValidationError::MissingPrimaryBin => Self::MissingPrimaryBin,
+ RadrootsOperationalListingValidationError::InvalidBin => Self::InvalidBin,
+ RadrootsOperationalListingValidationError::MissingPrice => Self::MissingPrice,
+ RadrootsOperationalListingValidationError::InvalidPrice => Self::InvalidPrice,
+ RadrootsOperationalListingValidationError::MissingInventory => Self::MissingInventory,
+ RadrootsOperationalListingValidationError::InvalidInventory => Self::InvalidInventory,
+ RadrootsOperationalListingValidationError::MissingAvailability => {
+ Self::MissingAvailability
+ }
+ RadrootsOperationalListingValidationError::MissingLocation => Self::MissingLocation,
+ RadrootsOperationalListingValidationError::MissingLocationLocality => {
+ Self::MissingLocationLocality
+ }
+ RadrootsOperationalListingValidationError::MissingLocationGeohash => {
+ Self::MissingLocationGeohash
+ }
+ RadrootsOperationalListingValidationError::InvalidLocationGeohash => {
+ Self::InvalidLocationGeohash
+ }
+ RadrootsOperationalListingValidationError::MissingDeliveryMethod => {
+ Self::MissingDeliveryMethod
+ }
+ }
+ }
+}
+
+#[cfg(feature = "runtime")]
+#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
+#[serde(rename_all = "snake_case")]
+#[non_exhaustive]
pub enum RadrootsSdkTradeErrorKind {
InvalidEnvelope,
InvalidCommandBody,
@@ -221,7 +308,7 @@ pub const RADROOTS_SDK_ERROR_CATALOG: &[RadrootsSdkErrorCatalogEntry] = &[
"signer_returned_event_drift",
RadrootsSdkErrorClass::Authorization,
false,
- RECOVERY_SELECT_AUTHORIZED_ACTOR,
+ RECOVERY_CONFIGURE_SIGNER,
),
RadrootsSdkErrorCatalogEntry::new(
"empty_transport_targets",
@@ -356,6 +443,12 @@ pub const RADROOTS_SDK_ERROR_CATALOG: &[RadrootsSdkErrorCatalogEntry] = &[
RECOVERY_FIX_REQUEST,
),
RadrootsSdkErrorCatalogEntry::new(
+ "listing_validation",
+ RadrootsSdkErrorClass::Request,
+ false,
+ RECOVERY_FIX_REQUEST,
+ ),
+ RadrootsSdkErrorCatalogEntry::new(
"listing_mutation",
RadrootsSdkErrorClass::Request,
false,
@@ -537,6 +630,10 @@ pub enum RadrootsSdkError {
ListingEdit {
message: String,
},
+ ListingValidation {
+ kind: RadrootsSdkListingValidationErrorKind,
+ message: String,
+ },
ListingMutation {
message: String,
},
@@ -594,6 +691,7 @@ impl RadrootsSdkError {
Self::InvalidRequest { .. } => "invalid_request",
Self::UnsupportedProfileSchema { .. } => "unsupported_profile_schema",
Self::ListingEdit { .. } => "listing_edit",
+ Self::ListingValidation { .. } => "listing_validation",
Self::ListingMutation { .. } => "listing_mutation",
Self::Outbox { .. } => "outbox",
Self::PrivateStore { .. } => "private_store",
@@ -647,6 +745,7 @@ impl RadrootsSdkError {
| Self::InvalidRequest { .. }
| Self::UnsupportedProfileSchema { .. }
| Self::ListingEdit { .. }
+ | Self::ListingValidation { .. }
| Self::ListingMutation { .. } => RadrootsSdkErrorClass::Request,
Self::ProductSyncUnsupported { .. } | Self::ReticulumTransportUnavailable { .. } => {
RadrootsSdkErrorClass::Unsupported
@@ -704,9 +803,10 @@ impl RadrootsSdkError {
Self::UnauthorizedActor { .. }
| Self::SignerPubkeyMismatch { .. }
| Self::SignerRequestRejected { .. }
- | Self::SignerReturnedEventDrift { .. }
| Self::Authority { .. } => vec![RadrootsSdkRecoveryAction::SelectAuthorizedActor],
- Self::SignerUnavailable { .. } => vec![RadrootsSdkRecoveryAction::ConfigureSigner],
+ Self::SignerUnavailable { .. } | Self::SignerReturnedEventDrift { .. } => {
+ vec![RadrootsSdkRecoveryAction::ConfigureSigner]
+ }
Self::EmptyTransportTargets { .. }
| Self::TransportTargetLimitExceeded { .. }
| Self::InvalidRelayUrl { .. } => {
@@ -740,6 +840,7 @@ impl RadrootsSdkError {
| Self::SignerProtocol { .. }
| Self::InvalidRequest { .. }
| Self::ListingEdit { .. }
+ | Self::ListingValidation { .. }
| Self::ListingMutation { .. } => vec![RadrootsSdkRecoveryAction::FixRequest],
}
}
@@ -823,6 +924,9 @@ impl RadrootsSdkError {
"endpoint_uri": endpoint_uri,
"behavior": behavior.as_str()
}),
+ Self::ListingValidation { kind, message } => {
+ json!({ "kind": kind, "message": message })
+ }
Self::ProductSyncTransportSetupFailure { message }
| Self::Authority { message }
| Self::EventStore { message }
@@ -989,6 +1093,9 @@ impl fmt::Display for RadrootsSdkError {
path.display()
),
Self::ListingEdit { message } => write!(f, "sdk listing edit error: {message}"),
+ Self::ListingValidation { kind, message } => {
+ write!(f, "sdk listing validation error ({kind:?}): {message}")
+ }
Self::ListingMutation { message } => {
write!(f, "sdk listing mutation error: {message}")
}
@@ -1093,15 +1200,21 @@ impl From<radroots_geocoder::GeocoderError> for RadrootsSdkError {
}
#[cfg(feature = "runtime")]
-impl From<radroots_trade::listing::RadrootsListingEditError> for RadrootsSdkError {
- fn from(error: radroots_trade::listing::RadrootsListingEditError) -> Self {
+impl From<radroots_trade::operational_listing::RadrootsOperationalListingEditError>
+ for RadrootsSdkError
+{
+ fn from(
+ error: radroots_trade::operational_listing::RadrootsOperationalListingEditError,
+ ) -> Self {
match error {
- radroots_trade::listing::RadrootsListingEditError::ActorRoleUnsatisfied {
- required_role,
- } => Self::UnauthorizedActor {
+ radroots_trade::operational_listing::RadrootsOperationalListingEditError::ActorRoleUnsatisfied { required_role } => Self::UnauthorizedActor {
operation: "listing.prepare_publish".to_owned(),
reason: format!("missing role {required_role:?}"),
},
+ radroots_trade::operational_listing::RadrootsOperationalListingEditError::InvalidModel(error) => Self::ListingValidation {
+ kind: RadrootsSdkListingValidationErrorKind::from(&error),
+ message: error.to_string(),
+ },
error => Self::ListingEdit {
message: error.to_string(),
},
@@ -1110,8 +1223,12 @@ impl From<radroots_trade::listing::RadrootsListingEditError> for RadrootsSdkErro
}
#[cfg(feature = "runtime")]
-impl From<radroots_trade::listing::RadrootsListingMutationError> for RadrootsSdkError {
- fn from(error: radroots_trade::listing::RadrootsListingMutationError) -> Self {
+impl From<radroots_trade::operational_listing::RadrootsOperationalListingMutationError>
+ for RadrootsSdkError
+{
+ fn from(
+ error: radroots_trade::operational_listing::RadrootsOperationalListingMutationError,
+ ) -> Self {
Self::ListingMutation {
message: error.to_string(),
}
diff --git a/crates/sdk/src/farms_runtime.rs b/crates/sdk/src/farms_runtime.rs
@@ -20,7 +20,7 @@ use radroots_event::{
farm::{RadrootsFarm, RadrootsFarmPublicLocation},
ids::{RadrootsAddressableCoordinate, RadrootsEventId},
kinds::KIND_FARM,
- listing::RadrootsListingPublicLocation,
+ operational_listing::RadrootsOperationalListingPublicLocation,
};
#[cfg(feature = "runtime")]
#[cfg(feature = "runtime")]
@@ -124,10 +124,29 @@ impl FarmEnqueuePublishRequest {
#[cfg(feature = "runtime")]
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
pub struct FarmPublishPlan {
- pub farm_addr: RadrootsAddressableCoordinate,
- pub expected_event_id: RadrootsEventId,
- pub frozen_draft: RadrootsEventDraft,
- pub created_at: RadrootsSdkTimestamp,
+ farm_addr: RadrootsAddressableCoordinate,
+ expected_event_id: RadrootsEventId,
+ frozen_draft: RadrootsEventDraft,
+ created_at: RadrootsSdkTimestamp,
+}
+
+#[cfg(feature = "runtime")]
+impl FarmPublishPlan {
+ pub fn farm_addr(&self) -> &RadrootsAddressableCoordinate {
+ &self.farm_addr
+ }
+
+ pub fn expected_event_id(&self) -> &RadrootsEventId {
+ &self.expected_event_id
+ }
+
+ pub fn frozen_draft(&self) -> &RadrootsEventDraft {
+ &self.frozen_draft
+ }
+
+ pub fn created_at(&self) -> RadrootsSdkTimestamp {
+ self.created_at
+ }
}
#[cfg(feature = "runtime")]
@@ -182,8 +201,8 @@ impl SdkPublicLocality {
}
}
- pub fn to_listing_public_location(&self) -> RadrootsListingPublicLocation {
- RadrootsListingPublicLocation {
+ pub fn to_listing_public_location(&self) -> RadrootsOperationalListingPublicLocation {
+ RadrootsOperationalListingPublicLocation {
primary: self.primary.clone(),
city: self.city.clone(),
region: self.region.clone(),
@@ -473,6 +492,7 @@ impl<'sdk> FarmsClient<'sdk> {
target_policy: TargetPolicy,
idempotency_key: Option<SdkIdempotencyKey>,
) -> Result<FarmEnqueueReceipt, RadrootsSdkError> {
+ let metadata = validate_farm_publish_plan(&plan)?;
let enqueue = enqueue_configured_signed_workflow(
self.sdk,
SdkWorkflowEnqueueRequest {
@@ -485,7 +505,7 @@ impl<'sdk> FarmsClient<'sdk> {
},
)
.await?;
- Ok(farm_enqueue_receipt(plan, enqueue))
+ Ok(farm_enqueue_receipt(metadata, enqueue))
}
pub async fn enqueue_prepared_publish_with_explicit_signer(
@@ -496,6 +516,7 @@ impl<'sdk> FarmsClient<'sdk> {
idempotency_key: Option<SdkIdempotencyKey>,
signer: &dyn RadrootsEventSigner,
) -> Result<FarmEnqueueReceipt, RadrootsSdkError> {
+ let metadata = validate_farm_publish_plan(&plan)?;
let enqueue = enqueue_signed_workflow(
self.sdk,
SdkWorkflowEnqueueRequest {
@@ -509,7 +530,7 @@ impl<'sdk> FarmsClient<'sdk> {
signer,
)
.await?;
- Ok(farm_enqueue_receipt(plan, enqueue))
+ Ok(farm_enqueue_receipt(metadata, enqueue))
}
pub async fn upsert_private_location(
@@ -710,12 +731,12 @@ impl<'sdk> FarmsClient<'sdk> {
#[cfg(feature = "runtime")]
fn farm_enqueue_receipt(
- plan: FarmPublishPlan,
+ metadata: ValidatedFarmPublishPlanMetadata,
enqueue: crate::workflow_runtime::SdkWorkflowEnqueueReceipt,
) -> FarmEnqueueReceipt {
FarmEnqueueReceipt {
- farm_addr: plan.farm_addr,
- expected_event_id: plan.expected_event_id,
+ farm_addr: metadata.farm_addr,
+ expected_event_id: metadata.expected_event_id,
signed_event_id: enqueue.signed_event_id,
local_event_seq: enqueue.local_event_seq,
outbox_operation_id: enqueue.outbox_operation_id,
@@ -726,6 +747,62 @@ fn farm_enqueue_receipt(
}
#[cfg(feature = "runtime")]
+struct ValidatedFarmPublishPlanMetadata {
+ farm_addr: RadrootsAddressableCoordinate,
+ expected_event_id: RadrootsEventId,
+}
+
+#[cfg(feature = "runtime")]
+fn validate_farm_publish_plan(
+ plan: &FarmPublishPlan,
+) -> Result<ValidatedFarmPublishPlanMetadata, RadrootsSdkError> {
+ let invalid = |reason: &str| RadrootsSdkError::InvalidRequest {
+ message: format!("invalid prepared farm publish plan: {reason}"),
+ };
+ plan.frozen_draft
+ .validate_for_signing()
+ .map_err(|_| invalid("frozen draft is invalid"))?;
+ if plan.frozen_draft.contract_id() != FARM_PROFILE_CONTRACT_ID
+ || plan.frozen_draft.kind_u32() != KIND_FARM
+ {
+ return Err(invalid("contract or kind does not match Farm publish"));
+ }
+
+ let expected_event_id = RadrootsEventId::parse(plan.frozen_draft.expected_event_id_str())
+ .expect("validated frozen draft has a typed event ID");
+ if plan.expected_event_id != expected_event_id {
+ return Err(invalid("expected event ID does not match frozen draft"));
+ }
+ if plan.created_at.unix_seconds() != plan.frozen_draft.created_at_u64() {
+ return Err(invalid("created-at timestamp does not match frozen draft"));
+ }
+
+ let tags = plan.frozen_draft.tags_as_vec();
+ let mut d_tags = tags
+ .iter()
+ .filter(|tag| tag.first().is_some_and(|value| value == "d"));
+ let d_tag = d_tags
+ .next()
+ .and_then(|tag| tag.get(1))
+ .ok_or_else(|| invalid("frozen draft is missing its farm identifier"))?;
+ if d_tags.next().is_some() {
+ return Err(invalid("frozen draft contains duplicate farm identifiers"));
+ }
+ let farm_addr = RadrootsAddressableCoordinate::parse(format!(
+ "{KIND_FARM}:{}:{d_tag}",
+ plan.frozen_draft.expected_pubkey_str()
+ ))
+ .map_err(|_| invalid("frozen draft farm address is invalid"))?;
+ if plan.farm_addr != farm_addr {
+ return Err(invalid("farm address does not match frozen draft"));
+ }
+ Ok(ValidatedFarmPublishPlanMetadata {
+ farm_addr,
+ expected_event_id,
+ })
+}
+
+#[cfg(feature = "runtime")]
fn farm_publish_plan(
actor: &RadrootsActorContext,
farm_value: RadrootsFarm,
diff --git a/crates/sdk/src/knowledge.rs b/crates/sdk/src/knowledge.rs
@@ -273,7 +273,7 @@ impl RadrootsWikiArticleBuilder {
}
pub fn build_event(self) -> Result<RadrootsNip01EventWireParts, RadrootsSdkKnowledgeError> {
- Ok(build_wiki_article_event(&self.build()?)?)
+ build_wiki_article_event(&self.build()?)
}
pub fn build_draft(
@@ -281,11 +281,7 @@ impl RadrootsWikiArticleBuilder {
expected_pubkey: impl AsRef<str>,
created_at: u32,
) -> Result<RadrootsEventDraft, RadrootsSdkKnowledgeError> {
- Ok(prepare_wiki_article_draft(
- &self.build()?,
- expected_pubkey,
- created_at,
- )?)
+ prepare_wiki_article_draft(&self.build()?, expected_pubkey, created_at)
}
}
@@ -317,7 +313,7 @@ impl RadrootsWikiRedirectBuilder {
}
pub fn build_event(self) -> Result<RadrootsNip01EventWireParts, RadrootsSdkKnowledgeError> {
- Ok(build_wiki_redirect_event(&self.build()?)?)
+ build_wiki_redirect_event(&self.build()?)
}
pub fn build_draft(
@@ -325,11 +321,7 @@ impl RadrootsWikiRedirectBuilder {
expected_pubkey: impl AsRef<str>,
created_at: u32,
) -> Result<RadrootsEventDraft, RadrootsSdkKnowledgeError> {
- Ok(prepare_wiki_redirect_draft(
- &self.build()?,
- expected_pubkey,
- created_at,
- )?)
+ prepare_wiki_redirect_draft(&self.build()?, expected_pubkey, created_at)
}
}
@@ -396,7 +388,7 @@ impl RadrootsWikiMergeRequestBuilder {
}
pub fn build_event(self) -> Result<RadrootsNip01EventWireParts, RadrootsSdkKnowledgeError> {
- Ok(build_wiki_merge_request_event(&self.build()?)?)
+ build_wiki_merge_request_event(&self.build()?)
}
pub fn build_draft(
@@ -404,11 +396,7 @@ impl RadrootsWikiMergeRequestBuilder {
expected_pubkey: impl AsRef<str>,
created_at: u32,
) -> Result<RadrootsEventDraft, RadrootsSdkKnowledgeError> {
- Ok(prepare_wiki_merge_request_draft(
- &self.build()?,
- expected_pubkey,
- created_at,
- )?)
+ prepare_wiki_merge_request_draft(&self.build()?, expected_pubkey, created_at)
}
}
@@ -512,7 +500,7 @@ impl RadrootsKnowledgeSourceBuilder {
}
pub fn build_event(self) -> Result<RadrootsNip01EventWireParts, RadrootsSdkKnowledgeError> {
- Ok(build_knowledge_source_event(&self.build()?)?)
+ build_knowledge_source_event(&self.build()?)
}
pub fn build_draft(
@@ -520,11 +508,7 @@ impl RadrootsKnowledgeSourceBuilder {
expected_pubkey: impl AsRef<str>,
created_at: u32,
) -> Result<RadrootsEventDraft, RadrootsSdkKnowledgeError> {
- Ok(prepare_knowledge_source_draft(
- &self.build()?,
- expected_pubkey,
- created_at,
- )?)
+ prepare_knowledge_source_draft(&self.build()?, expected_pubkey, created_at)
}
}
@@ -606,7 +590,7 @@ impl RadrootsKnowledgeClaimBuilder {
}
pub fn build_event(self) -> Result<RadrootsNip01EventWireParts, RadrootsSdkKnowledgeError> {
- Ok(build_knowledge_claim_event(&self.build()?)?)
+ build_knowledge_claim_event(&self.build()?)
}
pub fn build_draft(
@@ -614,11 +598,7 @@ impl RadrootsKnowledgeClaimBuilder {
expected_pubkey: impl AsRef<str>,
created_at: u32,
) -> Result<RadrootsEventDraft, RadrootsSdkKnowledgeError> {
- Ok(prepare_knowledge_claim_draft(
- &self.build()?,
- expected_pubkey,
- created_at,
- )?)
+ prepare_knowledge_claim_draft(&self.build()?, expected_pubkey, created_at)
}
}
@@ -692,7 +672,7 @@ impl RadrootsKnowledgeRelationBuilder {
}
pub fn build_event(self) -> Result<RadrootsNip01EventWireParts, RadrootsSdkKnowledgeError> {
- Ok(build_knowledge_relation_event(&self.build()?)?)
+ build_knowledge_relation_event(&self.build()?)
}
pub fn build_draft(
@@ -700,11 +680,7 @@ impl RadrootsKnowledgeRelationBuilder {
expected_pubkey: impl AsRef<str>,
created_at: u32,
) -> Result<RadrootsEventDraft, RadrootsSdkKnowledgeError> {
- Ok(prepare_knowledge_relation_draft(
- &self.build()?,
- expected_pubkey,
- created_at,
- )?)
+ prepare_knowledge_relation_draft(&self.build()?, expected_pubkey, created_at)
}
}
@@ -775,7 +751,7 @@ impl RadrootsKnowledgeReviewBuilder {
}
pub fn build_event(self) -> Result<RadrootsNip01EventWireParts, RadrootsSdkKnowledgeError> {
- Ok(build_knowledge_review_event(&self.build()?)?)
+ build_knowledge_review_event(&self.build()?)
}
pub fn build_draft(
@@ -783,11 +759,7 @@ impl RadrootsKnowledgeReviewBuilder {
expected_pubkey: impl AsRef<str>,
created_at: u32,
) -> Result<RadrootsEventDraft, RadrootsSdkKnowledgeError> {
- Ok(prepare_knowledge_review_draft(
- &self.build()?,
- expected_pubkey,
- created_at,
- )?)
+ prepare_knowledge_review_draft(&self.build()?, expected_pubkey, created_at)
}
}
@@ -874,7 +846,7 @@ impl RadrootsKnowledgeFieldReportBuilder {
}
pub fn build_event(self) -> Result<RadrootsNip01EventWireParts, RadrootsSdkKnowledgeError> {
- Ok(build_knowledge_field_report_event(&self.build()?)?)
+ build_knowledge_field_report_event(&self.build()?)
}
pub fn build_draft(
@@ -882,11 +854,7 @@ impl RadrootsKnowledgeFieldReportBuilder {
expected_pubkey: impl AsRef<str>,
created_at: u32,
) -> Result<RadrootsEventDraft, RadrootsSdkKnowledgeError> {
- Ok(prepare_knowledge_field_report_draft(
- &self.build()?,
- expected_pubkey,
- created_at,
- )?)
+ prepare_knowledge_field_report_draft(&self.build()?, expected_pubkey, created_at)
}
}
@@ -1251,6 +1219,13 @@ fn prepare_draft(
fn draft_error_code(error: &RadrootsDraftError) -> &'static str {
match error {
RadrootsDraftError::UnknownContract(_) => "unknown_contract",
+ RadrootsDraftError::ContractNotDraftAuthorable { .. } => "contract_not_draft_authorable",
+ RadrootsDraftError::ContractRegistryVersionMismatch { .. } => {
+ "contract_registry_version_mismatch"
+ }
+ RadrootsDraftError::DraftExpectedEventIdMismatch { .. } => {
+ "draft_expected_event_id_mismatch"
+ }
RadrootsDraftError::ContractKindMismatch { .. } => "contract_kind_mismatch",
RadrootsDraftError::ContractShape { error, .. } => error.code(),
RadrootsDraftError::SignedEventPubkeyMismatch { .. } => "signed_event_pubkey_mismatch",
diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs
@@ -60,8 +60,8 @@ pub use radroots_runtime_contract_v1 as runtime_contract_v1;
#[cfg(feature = "runtime")]
pub use crate::error::{
RADROOTS_SDK_ERROR_CATALOG, RadrootsSdkError, RadrootsSdkErrorCatalogEntry,
- RadrootsSdkErrorClass, RadrootsSdkGeoNamesErrorKind, RadrootsSdkRecoveryAction,
- RadrootsSdkTradeErrorKind, radroots_sdk_error_catalog,
+ RadrootsSdkErrorClass, RadrootsSdkGeoNamesErrorKind, RadrootsSdkListingValidationErrorKind,
+ RadrootsSdkRecoveryAction, RadrootsSdkTradeErrorKind, radroots_sdk_error_catalog,
};
#[cfg(feature = "runtime")]
pub use crate::farms_runtime::{
diff --git a/crates/sdk/src/listings_runtime.rs b/crates/sdk/src/listings_runtime.rs
@@ -11,42 +11,47 @@ use radroots_authority::{RadrootsActorContext, RadrootsEventSigner};
#[cfg(feature = "runtime")]
use radroots_event::{
draft::RadrootsEventDraft,
- ids::{RadrootsEventId, RadrootsListingAddress},
- listing::RadrootsListing,
+ ids::{RadrootsClassifiedListingAddress, RadrootsEventId},
+ kinds::KIND_CLASSIFIED_LISTING,
+ operational_listing::RadrootsOperationalListing,
};
#[cfg(feature = "runtime")]
use radroots_outbox::RadrootsOutboxEnqueueStatus;
#[cfg(feature = "runtime")]
-use radroots_trade::listing::{
- RadrootsCanonicalListingEdit, RadrootsListingEditDocumentV1, RadrootsListingMutation,
- build_listing_mutation_draft, canonicalize_listing_edit,
+use radroots_trade::operational_listing::{
+ RadrootsOperationalListingCanonicalEdit, RadrootsOperationalListingEditDocumentV1,
+ RadrootsOperationalListingMutation, build_operational_listing_mutation_draft,
+ canonicalize_operational_listing_edit,
};
#[cfg(feature = "runtime")]
pub const LISTING_PUBLISH_OPERATION_KIND: &str = "listing.publish.v1";
#[cfg(feature = "runtime")]
+const OPERATIONAL_LISTING_PUBLISHED_CONTRACT_ID: &str = "radroots.operational_listing.published.v1";
+
+#[cfg(feature = "runtime")]
#[derive(Clone, Debug, serde::Serialize)]
#[non_exhaustive]
pub struct ListingPreparePublishRequest {
#[serde(serialize_with = "crate::actor_json::serialize_actor_context")]
pub actor: RadrootsActorContext,
- pub document: RadrootsListingEditDocumentV1,
+ pub document: RadrootsOperationalListingEditDocumentV1,
pub created_at: Option<RadrootsSdkTimestamp>,
}
#[cfg(feature = "runtime")]
impl ListingPreparePublishRequest {
- pub fn new(actor: RadrootsActorContext, listing: RadrootsListing) -> Self {
+ pub fn new(actor: RadrootsActorContext, listing: RadrootsOperationalListing) -> Self {
Self {
actor,
- document: RadrootsListingEditDocumentV1::new(listing),
+ document: RadrootsOperationalListingEditDocumentV1::new(listing),
created_at: None,
}
}
pub fn from_document(
actor: RadrootsActorContext,
- document: RadrootsListingEditDocumentV1,
+ document: RadrootsOperationalListingEditDocumentV1,
) -> Self {
Self {
actor,
@@ -67,7 +72,7 @@ impl ListingPreparePublishRequest {
pub struct ListingEnqueuePublishRequest {
#[serde(serialize_with = "crate::actor_json::serialize_actor_context")]
pub actor: RadrootsActorContext,
- pub document: RadrootsListingEditDocumentV1,
+ pub document: RadrootsOperationalListingEditDocumentV1,
pub target_policy: TargetPolicy,
pub idempotency_key: Option<SdkIdempotencyKey>,
pub created_at: Option<RadrootsSdkTimestamp>,
@@ -77,19 +82,19 @@ pub struct ListingEnqueuePublishRequest {
impl ListingEnqueuePublishRequest {
pub fn new(
actor: RadrootsActorContext,
- listing: RadrootsListing,
+ listing: RadrootsOperationalListing,
target_policy: TargetPolicy,
) -> Self {
Self::from_document(
actor,
- RadrootsListingEditDocumentV1::new(listing),
+ RadrootsOperationalListingEditDocumentV1::new(listing),
target_policy,
)
}
pub fn from_document(
actor: RadrootsActorContext,
- document: RadrootsListingEditDocumentV1,
+ document: RadrootsOperationalListingEditDocumentV1,
target_policy: TargetPolicy,
) -> Self {
Self {
@@ -136,10 +141,29 @@ impl ListingEnqueuePublishRequest {
#[cfg(feature = "runtime")]
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
pub struct ListingPublishPlan {
- pub public_listing_addr: RadrootsListingAddress,
- pub expected_event_id: RadrootsEventId,
- pub frozen_draft: RadrootsEventDraft,
- pub created_at: RadrootsSdkTimestamp,
+ public_listing_addr: RadrootsClassifiedListingAddress,
+ expected_event_id: RadrootsEventId,
+ frozen_draft: RadrootsEventDraft,
+ created_at: RadrootsSdkTimestamp,
+}
+
+#[cfg(feature = "runtime")]
+impl ListingPublishPlan {
+ pub fn public_listing_addr(&self) -> &RadrootsClassifiedListingAddress {
+ &self.public_listing_addr
+ }
+
+ pub fn expected_event_id(&self) -> &RadrootsEventId {
+ &self.expected_event_id
+ }
+
+ pub fn frozen_draft(&self) -> &RadrootsEventDraft {
+ &self.frozen_draft
+ }
+
+ pub fn created_at(&self) -> RadrootsSdkTimestamp {
+ self.created_at
+ }
}
#[cfg(feature = "runtime")]
@@ -164,7 +188,7 @@ impl From<RadrootsOutboxEnqueueStatus> for SdkMutationState {
#[cfg(feature = "runtime")]
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
pub struct ListingEnqueueReceipt {
- pub public_listing_addr: RadrootsListingAddress,
+ pub public_listing_addr: RadrootsClassifiedListingAddress,
pub expected_event_id: RadrootsEventId,
pub signed_event_id: RadrootsEventId,
pub local_event_seq: i64,
@@ -242,6 +266,7 @@ impl<'sdk> ListingsClient<'sdk> {
target_policy: TargetPolicy,
idempotency_key: Option<SdkIdempotencyKey>,
) -> Result<ListingEnqueueReceipt, RadrootsSdkError> {
+ let metadata = validate_listing_publish_plan(&plan)?;
let enqueue = enqueue_configured_signed_workflow(
self.sdk,
SdkWorkflowEnqueueRequest {
@@ -254,7 +279,7 @@ impl<'sdk> ListingsClient<'sdk> {
},
)
.await?;
- Ok(listing_enqueue_receipt(plan, enqueue))
+ Ok(listing_enqueue_receipt(metadata, enqueue))
}
pub async fn enqueue_prepared_publish_with_explicit_signer(
@@ -265,6 +290,7 @@ impl<'sdk> ListingsClient<'sdk> {
idempotency_key: Option<SdkIdempotencyKey>,
signer: &dyn RadrootsEventSigner,
) -> Result<ListingEnqueueReceipt, RadrootsSdkError> {
+ let metadata = validate_listing_publish_plan(&plan)?;
let enqueue = enqueue_signed_workflow(
self.sdk,
SdkWorkflowEnqueueRequest {
@@ -278,7 +304,7 @@ impl<'sdk> ListingsClient<'sdk> {
signer,
)
.await?;
- Ok(listing_enqueue_receipt(plan, enqueue))
+ Ok(listing_enqueue_receipt(metadata, enqueue))
}
fn resolved_created_at(
@@ -294,12 +320,12 @@ impl<'sdk> ListingsClient<'sdk> {
#[cfg(feature = "runtime")]
fn listing_enqueue_receipt(
- plan: ListingPublishPlan,
+ metadata: ValidatedListingPublishPlanMetadata,
enqueue: crate::workflow_runtime::SdkWorkflowEnqueueReceipt,
) -> ListingEnqueueReceipt {
ListingEnqueueReceipt {
- public_listing_addr: plan.public_listing_addr,
- expected_event_id: plan.expected_event_id,
+ public_listing_addr: metadata.public_listing_addr,
+ expected_event_id: metadata.expected_event_id,
signed_event_id: enqueue.signed_event_id,
local_event_seq: enqueue.local_event_seq,
outbox_operation_id: enqueue.outbox_operation_id,
@@ -312,22 +338,83 @@ fn listing_enqueue_receipt(
#[cfg(feature = "runtime")]
fn canonical_listing_edit(
actor: &RadrootsActorContext,
- document: RadrootsListingEditDocumentV1,
-) -> Result<RadrootsCanonicalListingEdit, RadrootsSdkError> {
- canonicalize_listing_edit(actor, document).map_err(Into::into)
+ document: RadrootsOperationalListingEditDocumentV1,
+) -> Result<RadrootsOperationalListingCanonicalEdit, RadrootsSdkError> {
+ canonicalize_operational_listing_edit(actor, document).map_err(Into::into)
+}
+
+#[cfg(feature = "runtime")]
+struct ValidatedListingPublishPlanMetadata {
+ public_listing_addr: RadrootsClassifiedListingAddress,
+ expected_event_id: RadrootsEventId,
+}
+
+#[cfg(feature = "runtime")]
+fn validate_listing_publish_plan(
+ plan: &ListingPublishPlan,
+) -> Result<ValidatedListingPublishPlanMetadata, RadrootsSdkError> {
+ let invalid = |reason: &str| RadrootsSdkError::InvalidRequest {
+ message: format!("invalid prepared listing publish plan: {reason}"),
+ };
+ plan.frozen_draft
+ .validate_for_signing()
+ .map_err(|_| invalid("frozen draft is invalid"))?;
+ if plan.frozen_draft.contract_id() != OPERATIONAL_LISTING_PUBLISHED_CONTRACT_ID
+ || plan.frozen_draft.kind_u32() != KIND_CLASSIFIED_LISTING
+ {
+ return Err(invalid(
+ "contract or kind does not match Operational Listing publish",
+ ));
+ }
+
+ let expected_event_id = RadrootsEventId::parse(plan.frozen_draft.expected_event_id_str())
+ .expect("validated frozen draft has a typed event ID");
+ if plan.expected_event_id != expected_event_id {
+ return Err(invalid("expected event ID does not match frozen draft"));
+ }
+ if plan.created_at.unix_seconds() != plan.frozen_draft.created_at_u64() {
+ return Err(invalid("created-at timestamp does not match frozen draft"));
+ }
+
+ let tags = plan.frozen_draft.tags_as_vec();
+ let mut d_tags = tags
+ .iter()
+ .filter(|tag| tag.first().is_some_and(|value| value == "d"));
+ let d_tag = d_tags
+ .next()
+ .and_then(|tag| tag.get(1))
+ .ok_or_else(|| invalid("frozen draft is missing its listing identifier"))?;
+ if d_tags.next().is_some() {
+ return Err(invalid(
+ "frozen draft contains duplicate listing identifiers",
+ ));
+ }
+ let public_listing_addr = RadrootsClassifiedListingAddress::parse(format!(
+ "{KIND_CLASSIFIED_LISTING}:{}:{d_tag}",
+ plan.frozen_draft.expected_pubkey_str()
+ ))
+ .map_err(|_| invalid("frozen draft listing address is invalid"))?;
+ if plan.public_listing_addr != public_listing_addr {
+ return Err(invalid("listing address does not match frozen draft"));
+ }
+ Ok(ValidatedListingPublishPlanMetadata {
+ public_listing_addr,
+ expected_event_id,
+ })
}
#[cfg(feature = "runtime")]
fn listing_publish_plan(
actor: &RadrootsActorContext,
- document: RadrootsListingEditDocumentV1,
+ document: RadrootsOperationalListingEditDocumentV1,
created_at: RadrootsSdkTimestamp,
) -> Result<ListingPublishPlan, RadrootsSdkError> {
let created_at_nostr = created_at.try_into_nostr_created_at()?;
let canonical = canonical_listing_edit(actor, document)?;
let public_listing_addr = canonical.public_listing_addr().clone();
- let mutation = RadrootsListingMutation::publish(canonical);
- let frozen_draft = build_listing_mutation_draft(&mutation, u64::from(created_at_nostr))?;
+ let mutation = RadrootsOperationalListingMutation::publish(canonical);
+ let frozen_draft =
+ build_operational_listing_mutation_draft(&mutation, u64::from(created_at_nostr))?;
let expected_event_id = RadrootsEventId::parse(frozen_draft.expected_event_id_str())
.expect("frozen listing edit produces a valid event id");
Ok(ListingPublishPlan {
diff --git a/crates/sdk/src/private_store.rs b/crates/sdk/src/private_store.rs
@@ -579,12 +579,10 @@ impl SdkPrivateStore {
if record.deleted_at_ms.is_some()
|| record.trade_id != trade_id
|| record.artifact_kind != SdkPrivateTradeArtifactKind::BindingTerms
- {
- RadrootsTradePrivateTermsStateV1::Missing
- } else if record
- .candidate_id
- .as_deref()
- .is_some_and(|stored_candidate_id| stored_candidate_id != candidate_id)
+ || record
+ .candidate_id
+ .as_deref()
+ .is_some_and(|stored_candidate_id| stored_candidate_id != candidate_id)
{
RadrootsTradePrivateTermsStateV1::Missing
} else if record.schema_id != schema_id
diff --git a/crates/sdk/src/runtime.rs b/crates/sdk/src/runtime.rs
@@ -309,7 +309,8 @@ pub struct SdkSqliteWalCheckpointReceipt {
pub struct SdkEventStoreStorageStatus {
pub store: SdkSqliteStoreStatus,
pub total_events: i64,
- pub projection_eligible_events: i64,
+ #[serde(alias = "projection_eligible_events")]
+ pub valid_stream_events: i64,
pub transport_observations: i64,
pub last_event_seq: Option<i64>,
pub last_event_updated_at_ms: Option<i64>,
@@ -785,7 +786,7 @@ impl RadrootsClient {
event_store: SdkEventStoreStorageStatus {
store: event_store_status,
total_events: event_summary.total_events,
- projection_eligible_events: event_summary.projection_eligible_events,
+ valid_stream_events: event_summary.valid_stream_events,
transport_observations: event_summary.transport_observations,
last_event_seq: event_summary.last_event_seq,
last_event_updated_at_ms: event_summary.last_event_updated_at_ms,
@@ -1110,7 +1111,7 @@ async fn directory_storage_status_read_only(
event_store: SdkEventStoreStorageStatus {
store: event_store_status,
total_events: event_summary.total_events,
- projection_eligible_events: event_summary.projection_eligible_events,
+ valid_stream_events: event_summary.valid_stream_events,
transport_observations: event_summary.transport_observations,
last_event_seq: event_summary.last_event_seq,
last_event_updated_at_ms: event_summary.last_event_updated_at_ms,
@@ -1180,33 +1181,9 @@ async fn sqlite_store_status_from_pool(
async fn event_store_status_summary_from_pool(
pool: &SqlitePool,
) -> Result<radroots_event_store::RadrootsEventStoreStatusSummary, RadrootsSdkError> {
- let row = sqlx::query(
- "SELECT COUNT(*) AS total_events, COALESCE(SUM(CASE WHEN projection_eligible = 1 THEN 1 ELSE 0 END), 0) AS projection_eligible_events, MAX(seq) AS last_event_seq, MAX(updated_at_ms) AS last_event_updated_at_ms FROM event_envelopes",
- )
- .fetch_one(pool)
- .await
- .map_err(|error| SqliteStoreRole::EventStore.error(error.to_string()))?;
- let transport_observations = sqlite_query_i64(
- pool,
- "SELECT COUNT(*) FROM event_transport_observation",
- SqliteStoreRole::EventStore,
- )
- .await?;
- Ok(radroots_event_store::RadrootsEventStoreStatusSummary {
- total_events: row
- .try_get("total_events")
- .map_err(|error| SqliteStoreRole::EventStore.error(error.to_string()))?,
- projection_eligible_events: row
- .try_get("projection_eligible_events")
- .map_err(|error| SqliteStoreRole::EventStore.error(error.to_string()))?,
- transport_observations,
- last_event_seq: row
- .try_get("last_event_seq")
- .map_err(|error| SqliteStoreRole::EventStore.error(error.to_string()))?,
- last_event_updated_at_ms: row
- .try_get("last_event_updated_at_ms")
- .map_err(|error| SqliteStoreRole::EventStore.error(error.to_string()))?,
- })
+ radroots_event_store::inspect_event_store_status(pool)
+ .await
+ .map_err(|error| SqliteStoreRole::EventStore.error(error.to_string()))
}
#[cfg(feature = "runtime")]
@@ -2350,13 +2327,17 @@ async fn recover_sdk_runtime_state(pool: &SqlitePool, now_ms: i64) -> Result<(),
if operation_recovery.rows_affected() > 0 {
record_runtime_recovery_receipt(
pool,
- "signer_timeout",
- None,
- None,
- None,
- "retry_operation_with_same_idempotency_key",
- serde_json::json!({ "recovered_operations": operation_recovery.rows_affected() }),
- now_ms,
+ RuntimeRecoveryReceiptWrite {
+ recovery_code: "signer_timeout",
+ operation_kind: None,
+ actor_pubkey: None,
+ idempotency_key: None,
+ recovery_action: "retry_operation_with_same_idempotency_key",
+ detail_json: serde_json::json!({
+ "recovered_operations": operation_recovery.rows_affected()
+ }),
+ created_at_ms: now_ms,
+ },
)
.await?;
}
@@ -2371,13 +2352,17 @@ async fn recover_sdk_runtime_state(pool: &SqlitePool, now_ms: i64) -> Result<(),
if reservation_expiry.rows_affected() > 0 {
record_runtime_recovery_receipt(
pool,
- "reservation_expiry",
- None,
- None,
- None,
- "retry_operation_with_same_idempotency_key",
- serde_json::json!({ "expired_reservations": reservation_expiry.rows_affected() }),
- now_ms,
+ RuntimeRecoveryReceiptWrite {
+ recovery_code: "reservation_expiry",
+ operation_kind: None,
+ actor_pubkey: None,
+ idempotency_key: None,
+ recovery_action: "retry_operation_with_same_idempotency_key",
+ detail_json: serde_json::json!({
+ "expired_reservations": reservation_expiry.rows_affected()
+ }),
+ created_at_ms: now_ms,
+ },
)
.await?;
}
@@ -2391,13 +2376,17 @@ async fn recover_sdk_runtime_state(pool: &SqlitePool, now_ms: i64) -> Result<(),
if projection_stale.rows_affected() > 0 {
record_runtime_recovery_receipt(
pool,
- "projection_stale",
- None,
- None,
- None,
- "inspect_local_stores",
- serde_json::json!({ "stale_projection_checkpoints": projection_stale.rows_affected() }),
- now_ms,
+ RuntimeRecoveryReceiptWrite {
+ recovery_code: "projection_stale",
+ operation_kind: None,
+ actor_pubkey: None,
+ idempotency_key: None,
+ recovery_action: "inspect_local_stores",
+ detail_json: serde_json::json!({
+ "stale_projection_checkpoints": projection_stale.rows_affected()
+ }),
+ created_at_ms: now_ms,
+ },
)
.await?;
}
@@ -2412,13 +2401,17 @@ async fn recover_sdk_runtime_state(pool: &SqlitePool, now_ms: i64) -> Result<(),
if outbox_claim_recovery.rows_affected() > 0 {
record_runtime_recovery_receipt(
pool,
- "relay_failure",
- None,
- None,
- None,
- "retry_after_transport_failure",
- serde_json::json!({ "recovered_outbox_claims": outbox_claim_recovery.rows_affected() }),
- now_ms,
+ RuntimeRecoveryReceiptWrite {
+ recovery_code: "relay_failure",
+ operation_kind: None,
+ actor_pubkey: None,
+ idempotency_key: None,
+ recovery_action: "retry_after_transport_failure",
+ detail_json: serde_json::json!({
+ "recovered_outbox_claims": outbox_claim_recovery.rows_affected()
+ }),
+ created_at_ms: now_ms,
+ },
)
.await?;
}
@@ -2426,26 +2419,31 @@ async fn recover_sdk_runtime_state(pool: &SqlitePool, now_ms: i64) -> Result<(),
}
#[cfg(feature = "runtime")]
+pub(crate) struct RuntimeRecoveryReceiptWrite<'a> {
+ pub(crate) recovery_code: &'a str,
+ pub(crate) operation_kind: Option<&'a str>,
+ pub(crate) actor_pubkey: Option<&'a str>,
+ pub(crate) idempotency_key: Option<&'a str>,
+ pub(crate) recovery_action: &'a str,
+ pub(crate) detail_json: serde_json::Value,
+ pub(crate) created_at_ms: i64,
+}
+
+#[cfg(feature = "runtime")]
pub(crate) async fn record_runtime_recovery_receipt(
pool: &SqlitePool,
- recovery_code: &'static str,
- operation_kind: Option<&str>,
- actor_pubkey: Option<&str>,
- idempotency_key: Option<&str>,
- recovery_action: &'static str,
- detail_json: serde_json::Value,
- created_at_ms: i64,
+ receipt: RuntimeRecoveryReceiptWrite<'_>,
) -> Result<(), RadrootsSdkError> {
sqlx::query(
"INSERT INTO sdk_runtime_recovery_receipt(recovery_code, operation_kind, actor_pubkey, idempotency_key, recovery_action, detail_json, created_at_ms) VALUES (?, ?, ?, ?, ?, ?, ?)",
)
- .bind(recovery_code)
- .bind(operation_kind)
- .bind(actor_pubkey)
- .bind(idempotency_key)
- .bind(recovery_action)
- .bind(detail_json.to_string())
- .bind(created_at_ms)
+ .bind(receipt.recovery_code)
+ .bind(receipt.operation_kind)
+ .bind(receipt.actor_pubkey)
+ .bind(receipt.idempotency_key)
+ .bind(receipt.recovery_action)
+ .bind(receipt.detail_json.to_string())
+ .bind(receipt.created_at_ms)
.execute(pool)
.await
.map(|_| ())
diff --git a/crates/sdk/src/signer_provider.rs b/crates/sdk/src/signer_provider.rs
@@ -7,8 +7,8 @@ use radroots_authority::{
use radroots_event::draft::{RadrootsEventDraft, RadrootsSignedEvent};
use radroots_event::ids::RadrootsPublicKey;
use radroots_event::kinds::{
- KIND_FARM, KIND_LISTING, KIND_TRADE_CANCELLATION, KIND_TRADE_DECISION, KIND_TRADE_PROPOSAL,
- KIND_TRADE_REVISION_DECISION, KIND_TRADE_REVISION_PROPOSAL,
+ KIND_CLASSIFIED_LISTING, KIND_FARM, KIND_TRADE_CANCELLATION, KIND_TRADE_DECISION,
+ KIND_TRADE_PROPOSAL, KIND_TRADE_REVISION_DECISION, KIND_TRADE_REVISION_PROPOSAL,
};
use radroots_event::wire::RadrootsNip01EventWire;
use radroots_nostr::prelude::{RadrootsNostrEvent, RadrootsNostrKeys};
@@ -29,7 +29,7 @@ pub type RadrootsSdkLocalSignerCapability = dyn RadrootsEventSigner + Send + Syn
pub const RADROOTS_SDK_MYC_NIP46_PRODUCT_SIGN_EVENT_KINDS: [u32; 7] = [
KIND_FARM,
- KIND_LISTING,
+ KIND_CLASSIFIED_LISTING,
KIND_TRADE_PROPOSAL,
KIND_TRADE_DECISION,
KIND_TRADE_REVISION_PROPOSAL,
diff --git a/crates/sdk/src/sync_runtime.rs b/crates/sdk/src/sync_runtime.rs
@@ -121,7 +121,7 @@ pub enum SyncStatusSource {
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
pub struct SyncEventStoreStatus {
pub total_events: i64,
- pub projection_eligible_events: i64,
+ pub valid_stream_events: i64,
pub transport_observations: i64,
pub last_event_seq: Option<i64>,
pub last_event_updated_at_ms: Option<i64>,
@@ -132,7 +132,7 @@ impl From<RadrootsEventStoreStatusSummary> for SyncEventStoreStatus {
fn from(summary: RadrootsEventStoreStatusSummary) -> Self {
Self {
total_events: summary.total_events,
- projection_eligible_events: summary.projection_eligible_events,
+ valid_stream_events: summary.valid_stream_events,
transport_observations: summary.transport_observations,
last_event_seq: summary.last_event_seq,
last_event_updated_at_ms: summary.last_event_updated_at_ms,
@@ -1006,20 +1006,9 @@ fn reticulum_event_receipt(
#[cfg(feature = "runtime")]
fn reticulum_event_final_state(
_event_state: RadrootsOutboxEventState,
- targets: &[RadrootsOutboxDeliveryTargetRecord],
+ _targets: &[RadrootsOutboxDeliveryTargetRecord],
) -> PushOutboxEventState {
- if targets.iter().any(|target| {
- matches!(
- target.status,
- RadrootsOutboxDeliveryTargetStatus::DeferredUntilImplemented
- | RadrootsOutboxDeliveryTargetStatus::Pending
- | RadrootsOutboxDeliveryTargetStatus::FailedRetryable
- )
- }) {
- PushOutboxEventState::DeferredUntilImplemented
- } else {
- PushOutboxEventState::DeferredUntilImplemented
- }
+ PushOutboxEventState::DeferredUntilImplemented
}
#[cfg(feature = "runtime")]
@@ -1093,7 +1082,7 @@ pub(crate) async fn refresh_product_projections_for_sdk(
}
let refreshed_at_ms = sdk_now_ms(sdk)?;
let summary = sdk._event_store.status_summary().await?;
- let scanned_events = usize::try_from(summary.projection_eligible_events.max(0))
+ let scanned_events = usize::try_from(summary.valid_stream_events.max(0))
.unwrap_or(usize::MAX)
.min(request.limit as usize);
let trade_upserts = trade_mutation_count(sdk).await?;
@@ -1149,7 +1138,7 @@ fn projection_source_digest(
hasher.update(b"\0");
hasher.update(summary.total_events.to_string().as_bytes());
hasher.update(b"\0");
- hasher.update(summary.projection_eligible_events.to_string().as_bytes());
+ hasher.update(summary.valid_stream_events.to_string().as_bytes());
hasher.update(b"\0");
hasher.update(
summary
@@ -1648,18 +1637,11 @@ async fn complete_missing_radrootsd_delivery_target(
publish: &TransportPublishJobView,
now_ms: i64,
) -> Result<(), RadrootsSdkError> {
- if publish.status == TransportPublishJobStatus::DeliveryDeferred {
- sync.sdk
- ._outbox
- .mark_delivery_target_deferred_until_implemented(
- claimed.outbox_event_id,
- claimed.claim_token.as_str(),
- target.delivery_target_id,
- "radrootsd publish deferred until implemented",
- now_ms,
- )
- .await?;
- } else if publish.status == TransportPublishJobStatus::DeliveryDeferredUntilImplemented {
+ if matches!(
+ publish.status,
+ TransportPublishJobStatus::DeliveryDeferred
+ | TransportPublishJobStatus::DeliveryDeferredUntilImplemented
+ ) {
sync.sdk
._outbox
.mark_delivery_target_deferred_until_implemented(
@@ -1754,9 +1736,11 @@ fn radrootsd_transport_error_receipt(
fn radrootsd_push_event_final_state(publish: &TransportPublishJobView) -> PushOutboxEventState {
if publish.delivery_satisfied {
PushOutboxEventState::Published
- } else if publish.status == TransportPublishJobStatus::DeliveryDeferred {
- PushOutboxEventState::DeferredUntilImplemented
- } else if publish.status == TransportPublishJobStatus::DeliveryDeferredUntilImplemented {
+ } else if matches!(
+ publish.status,
+ TransportPublishJobStatus::DeliveryDeferred
+ | TransportPublishJobStatus::DeliveryDeferredUntilImplemented
+ ) {
PushOutboxEventState::DeferredUntilImplemented
} else if publish.retryable_count > 0 || !publish.terminal {
PushOutboxEventState::PublishRetryable
diff --git a/crates/sdk/src/trade_runtime.rs b/crates/sdk/src/trade_runtime.rs
@@ -905,20 +905,14 @@ pub enum TradeListAnyOf {
}
#[cfg(feature = "runtime")]
-#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TradeListSort {
+ #[default]
UpdatedDesc,
}
#[cfg(feature = "runtime")]
-impl Default for TradeListSort {
- fn default() -> Self {
- Self::UpdatedDesc
- }
-}
-
-#[cfg(feature = "runtime")]
#[derive(Clone, Debug, Serialize)]
#[non_exhaustive]
pub struct ListTradesRequest {
diff --git a/crates/sdk/src/workflow_runtime.rs b/crates/sdk/src/workflow_runtime.rs
@@ -3,15 +3,17 @@ use crate::RadrootsSdkSignRequest;
use crate::{
RadrootsClient, RadrootsSdkError, ReticulumBehavior, SatisfactionPolicy, SdkIdempotencyKey,
TargetPolicy, TargetSet, TransportProfile,
- runtime::{record_runtime_recovery_receipt, sdk_now_ms},
+ runtime::{RuntimeRecoveryReceiptWrite, record_runtime_recovery_receipt, sdk_now_ms},
};
use radroots_authority::{RadrootsActorContext, RadrootsEventSigner, sign_authorized_draft};
use radroots_event::{
+ RadrootsEventKind, RadrootsEventKindClass,
draft::{RadrootsEventDraft, RadrootsSignedEvent},
ids::RadrootsEventId,
};
use radroots_event_store::{
- RadrootsEventIngest, RadrootsTransportObservation, RadrootsTransportObservationType,
+ RadrootsEventIngest, RadrootsEventPersistence, RadrootsEventStoreError,
+ RadrootsTransportObservation, RadrootsTransportObservationType,
};
use radroots_outbox::{
RadrootsOutboxDeliveryPlanInput, RadrootsOutboxEnqueueStatus, RadrootsOutboxReticulumBehavior,
@@ -50,6 +52,7 @@ pub(crate) async fn enqueue_signed_workflow(
request: SdkWorkflowEnqueueRequest<'_>,
signer: &dyn RadrootsEventSigner,
) -> Result<SdkWorkflowEnqueueReceipt, RadrootsSdkError> {
+ ensure_durable_workflow_kind(&request)?;
let delivery_plan =
resolved_delivery_plan(sdk, &request.target_policy, &request.satisfaction_policy)?;
let prepared = prepare_runtime_operation_journal(sdk, &request, &delivery_plan).await?;
@@ -88,6 +91,7 @@ pub(crate) async fn enqueue_configured_signed_workflow(
sdk: &RadrootsClient,
request: SdkWorkflowEnqueueRequest<'_>,
) -> Result<SdkWorkflowEnqueueReceipt, RadrootsSdkError> {
+ ensure_durable_workflow_kind(&request)?;
let delivery_plan =
resolved_delivery_plan(sdk, &request.target_policy, &request.satisfaction_policy)?;
let prepared = prepare_runtime_operation_journal(sdk, &request, &delivery_plan).await?;
@@ -167,19 +171,24 @@ async fn enqueue_signed_workflow_event(
RadrootsTransportObservationType::LocalImport,
observed_at_ms,
)?;
- let ingest = RadrootsEventIngest::new(signed_event.clone(), observed_at_ms)
- .with_observation(local_import_observation);
+ let ingest =
+ workflow_event_ingest(request.operation_kind, signed_event.clone(), observed_at_ms)?
+ .with_observation(local_import_observation);
let ingest_receipt = sdk
._event_store
.ingest_event_in_transaction(&mut tx, ingest)
.await?;
+ let (event_store_inserted, local_event_seq) = durable_event_persistence(
+ ingest_receipt.event_id.as_str(),
+ &ingest_receipt.persistence,
+ )?;
let outbox_input = signed_outbox_input(
request.operation_kind,
request.frozen_draft,
signed_event,
delivery_plan_value,
idempotency_key.clone(),
- ingest_receipt.inserted,
+ event_store_inserted,
observed_at_ms,
);
let outbox_receipt = sdk
@@ -190,7 +199,7 @@ async fn enqueue_signed_workflow_event(
digest_prefix(outbox_receipt.operation_idempotency_digest.as_str());
let receipt = SdkWorkflowEnqueueReceipt {
signed_event_id,
- local_event_seq: ingest_receipt.seq,
+ local_event_seq,
outbox_operation_id: outbox_receipt.operation_id,
outbox_event_id: outbox_receipt.outbox_event_id,
state: outbox_receipt.status,
@@ -241,12 +250,17 @@ async fn enqueue_signed_trade_workflow_event(
RadrootsTransportObservationType::LocalImport,
observed_at_ms,
)?;
- let ingest = RadrootsEventIngest::new(signed_event.clone(), observed_at_ms)
- .with_observation(local_import_observation);
+ let ingest =
+ workflow_event_ingest(request.operation_kind, signed_event.clone(), observed_at_ms)?
+ .with_observation(local_import_observation);
let ingest_receipt = sdk
._event_store
.ingest_event_in_transaction(&mut tx, ingest)
.await?;
+ let (event_store_inserted, local_event_seq) = durable_event_persistence(
+ ingest_receipt.event_id.as_str(),
+ &ingest_receipt.persistence,
+ )?;
tx.commit()
.await
.map_err(|error| RadrootsSdkError::EventStore {
@@ -258,7 +272,7 @@ async fn enqueue_signed_trade_workflow_event(
signed_event,
delivery_plan_value,
idempotency_key.clone(),
- ingest_receipt.inserted,
+ event_store_inserted,
observed_at_ms,
)?;
let outbox_receipt = sdk
@@ -269,7 +283,7 @@ async fn enqueue_signed_trade_workflow_event(
digest_prefix(outbox_receipt.operation_idempotency_digest.as_str());
let receipt = SdkWorkflowEnqueueReceipt {
signed_event_id,
- local_event_seq: ingest_receipt.seq,
+ local_event_seq,
outbox_operation_id: outbox_receipt.operation_id,
outbox_event_id: outbox_receipt.outbox_event_id,
state: outbox_receipt.status,
@@ -418,6 +432,58 @@ fn digest_prefix(digest: &str) -> String {
digest.chars().take(12).collect()
}
+fn ensure_durable_workflow_kind(
+ request: &SdkWorkflowEnqueueRequest<'_>,
+) -> Result<(), RadrootsSdkError> {
+ if RadrootsEventKind::new(request.frozen_draft.kind_u32()).class()
+ == RadrootsEventKindClass::Ephemeral
+ {
+ return Err(RadrootsSdkError::InvalidRequest {
+ message: format!(
+ "{} cannot enqueue ephemeral event kind {} into a durable workflow",
+ request.operation_kind,
+ request.frozen_draft.kind_u32()
+ ),
+ });
+ }
+ Ok(())
+}
+
+fn workflow_event_ingest(
+ operation_kind: &str,
+ signed_event: RadrootsSignedEvent,
+ observed_at_ms: i64,
+) -> Result<RadrootsEventIngest, RadrootsSdkError> {
+ RadrootsEventIngest::from_signed_event(signed_event, observed_at_ms).map_err(
+ |error| match error {
+ RadrootsEventStoreError::Nip01Verification(error) => {
+ RadrootsSdkError::SignerReturnedEventDrift {
+ operation: operation_kind.to_owned(),
+ reason: format!(
+ "signer returned an event that failed NIP-01 verification: {error}"
+ ),
+ }
+ }
+ error => error.into(),
+ },
+ )
+}
+
+fn durable_event_persistence(
+ event_id: &str,
+ persistence: &RadrootsEventPersistence,
+) -> Result<(bool, i64), RadrootsSdkError> {
+ match persistence {
+ RadrootsEventPersistence::Inserted { seq } => Ok((true, *seq)),
+ RadrootsEventPersistence::Duplicate { seq } => Ok((false, *seq)),
+ RadrootsEventPersistence::NotPersisted => Err(RadrootsSdkError::InvalidRequest {
+ message: format!(
+ "workflow event `{event_id}` requires durable local event-store persistence"
+ ),
+ }),
+ }
+}
+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum SdkRuntimeOperationState {
Prepared,
@@ -727,13 +793,15 @@ async fn record_runtime_operation_failure(
if let Some((recovery_code, recovery_action)) = recovery {
record_runtime_recovery_receipt(
sdk._event_store.pool(),
- recovery_code,
- Some(request.operation_kind),
- Some(request.actor.pubkey().as_str()),
- Some(idempotency_key.as_str()),
- recovery_action,
- error.detail_json(),
- sdk_now_ms(sdk)?,
+ RuntimeRecoveryReceiptWrite {
+ recovery_code,
+ operation_kind: Some(request.operation_kind),
+ actor_pubkey: Some(request.actor.pubkey().as_str()),
+ idempotency_key: Some(idempotency_key.as_str()),
+ recovery_action,
+ detail_json: error.detail_json(),
+ created_at_ms: sdk_now_ms(sdk)?,
+ },
)
.await?;
}
diff --git a/crates/sdk/tests/farms_runtime.rs b/crates/sdk/tests/farms_runtime.rs
@@ -1,11 +1,8 @@
#![cfg(feature = "runtime")]
-use radroots_authority::{
- RadrootsActorContext, RadrootsEventSigner, RadrootsSignerError, RadrootsSignerIdentity,
-};
+use radroots_authority::RadrootsActorContext;
use radroots_event::{
contract::RadrootsActorRole,
- draft::{RadrootsEventDraft, RadrootsSignedEvent, RadrootsSignedEventParts},
farm::RadrootsFarm,
ids::RadrootsAddressableCoordinate,
kinds::{KIND_FARM, KIND_PROFILE},
@@ -30,13 +27,14 @@ use radroots_sdk::{
use radroots_transport_nostr::{RadrootsMockRelayPublishAdapter, RadrootsNostrTransport};
use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
+#[path = "support/fixture_signer.rs"]
+mod fixture_signer;
#[path = "support/serializer_failure.rs"]
mod serializer_failure;
+use fixture_signer::{FixtureSigner, fixture_alice_pubkey, fixture_bob_pubkey};
use serializer_failure::assert_struct_serialize_error_paths;
-const FARMER: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
-const OTHER: &str = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
const FARM_A_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAAAA";
const FARM_B_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAAAQ";
const FARM_C_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAAAg";
@@ -46,66 +44,20 @@ const FARM_F_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAABQ";
const RELAY: &str = "wss://relay.example.com";
const RELAY_B: &str = "wss://relay-b.example.com";
-#[derive(Clone)]
-struct FixtureSigner {
- identity: RadrootsSignerIdentity,
-}
-
-impl FixtureSigner {
- fn new(pubkey: &str) -> Self {
- Self {
- identity: RadrootsSignerIdentity::new(pubkey).expect("identity"),
- }
- }
+fn farmer_pubkey() -> &'static str {
+ fixture_alice_pubkey()
}
-impl RadrootsEventSigner for FixtureSigner {
- fn pubkey(&self) -> &radroots_event::ids::RadrootsPublicKey {
- self.identity.pubkey()
- }
-
- fn sign_frozen_draft(
- &self,
- draft: &RadrootsEventDraft,
- ) -> Result<RadrootsSignedEvent, RadrootsSignerError> {
- if self.pubkey().as_str() != draft.expected_pubkey_str() {
- return Err(RadrootsSignerError::SigningFailed {
- message: "wrong fixture signer".to_owned(),
- });
- }
- let sig = "f".repeat(128);
- let raw_json = serde_json::json!({
- "id": draft.expected_event_id_str(),
- "pubkey": self.pubkey().as_str(),
- "created_at": draft.created_at_u64(),
- "kind": draft.kind_u32(),
- "tags": draft.tags_as_vec(),
- "content": draft.content(),
- "sig": sig,
- })
- .to_string();
- RadrootsSignedEvent::new(RadrootsSignedEventParts {
- id: draft.expected_event_id_str().to_owned(),
- pubkey: self.pubkey().as_str().to_owned(),
- created_at: draft.created_at_u64(),
- kind: draft.kind_u32(),
- tags: draft.tags_as_vec(),
- content: draft.content().to_owned(),
- sig,
- raw_json,
- })
- .map_err(|error| RadrootsSignerError::SigningFailed {
- message: error.to_string(),
- })
- }
+fn other_pubkey() -> &'static str {
+ fixture_bob_pubkey()
}
fn farmer_actor() -> RadrootsActorContext {
- RadrootsActorContext::test(FARMER, [RadrootsActorRole::Farmer]).expect("actor")
+ RadrootsActorContext::test(farmer_pubkey(), [RadrootsActorRole::Farmer]).expect("actor")
}
fn non_farmer_actor() -> RadrootsActorContext {
- RadrootsActorContext::test(FARMER, [RadrootsActorRole::Buyer]).expect("actor")
+ RadrootsActorContext::test(farmer_pubkey(), [RadrootsActorRole::Buyer]).expect("actor")
}
fn farm(d_tag: &str, name: &str) -> RadrootsFarm {
@@ -252,15 +204,15 @@ async fn farm_prepare_publish_is_side_effect_free() {
let request = FarmPreparePublishRequest::new(farmer_actor(), farm(FARM_A_D_TAG, "North Farm"));
let prepared = sdk.farms().prepare_publish(request).expect("prepared");
- assert_eq!(prepared.frozen_draft.kind_u32(), KIND_FARM);
- assert_eq!(prepared.created_at.unix_seconds(), 1_700_000_000);
+ assert_eq!(prepared.frozen_draft().kind_u32(), KIND_FARM);
+ assert_eq!(prepared.created_at().unix_seconds(), 1_700_000_000);
assert_eq!(
- prepared.expected_event_id,
- prepared.frozen_draft.expected_event_id_str()
+ prepared.expected_event_id().as_str(),
+ prepared.frozen_draft().expected_event_id_str()
);
assert_eq!(
- prepared.farm_addr.as_str(),
- format!("{KIND_FARM}:{FARMER}:{FARM_A_D_TAG}")
+ prepared.farm_addr().as_str(),
+ format!("{KIND_FARM}:{}:{FARM_A_D_TAG}", farmer_pubkey())
);
let paths = sdk.storage_paths().expect("paths");
@@ -277,7 +229,7 @@ async fn farm_prepare_publish_is_side_effect_free() {
);
assert!(
event_store
- .get_event(prepared.expected_event_id.as_str())
+ .raw_event(prepared.expected_event_id().as_str())
.await
.expect("event lookup")
.is_none()
@@ -327,9 +279,9 @@ async fn farm_private_location_upsert_stores_exact_location_and_public_locality_
assert_eq!(
receipt.farm_addr.as_str(),
- format!("{KIND_FARM}:{FARMER}:{FARM_A_D_TAG}")
+ format!("{KIND_FARM}:{}:{FARM_A_D_TAG}", farmer_pubkey())
);
- assert_eq!(receipt.farm_pubkey, FARMER);
+ assert_eq!(receipt.farm_pubkey, farmer_pubkey());
assert_eq!(receipt.farm_d_tag, FARM_A_D_TAG);
assert_eq!(receipt.label, None);
assert_eq!(receipt.exact_location, SdkExactLocation::new(12.26, -34.51));
@@ -641,13 +593,13 @@ async fn farm_enqueue_publish_stores_event_and_queues_signed_outbox_without_prof
.expect("prepared");
let receipt = sdk
.farms()
- .enqueue_publish_with_explicit_signer(request, &FixtureSigner::new(FARMER))
+ .enqueue_publish_with_explicit_signer(request, &FixtureSigner::new(farmer_pubkey()))
.await
.expect("enqueue");
- assert_eq!(receipt.expected_event_id, prepared.expected_event_id);
+ assert_eq!(&receipt.expected_event_id, prepared.expected_event_id());
assert_eq!(receipt.signed_event_id, receipt.expected_event_id);
- assert_eq!(receipt.farm_addr, prepared.farm_addr);
+ assert_eq!(&receipt.farm_addr, prepared.farm_addr());
assert_eq!(receipt.local_event_seq, 1);
assert_eq!(receipt.outbox_operation_id, 1);
assert_eq!(receipt.outbox_event_id, 1);
@@ -664,10 +616,11 @@ async fn farm_enqueue_publish_stores_event_and_queues_signed_outbox_without_prof
.expect("event store status");
assert_eq!(status.total_events, 1);
let stored_event = event_store
- .get_event(receipt.signed_event_id.as_str())
+ .valid_event(receipt.signed_event_id.as_str())
.await
.expect("event lookup")
.expect("stored event");
+ let stored_event = stored_event.raw_event();
assert_eq!(stored_event.kind, KIND_FARM);
assert_ne!(stored_event.kind, KIND_PROFILE);
assert_eq!(
@@ -700,7 +653,7 @@ async fn farm_enqueue_publish_returns_sanitized_signer_errors_before_mutation()
.expect("idempotency key");
let error = sdk
.farms()
- .enqueue_publish_with_explicit_signer(request, &FixtureSigner::new(OTHER))
+ .enqueue_publish_with_explicit_signer(request, &FixtureSigner::new(other_pubkey()))
.await
.expect_err("signer error");
let message = error.to_string();
@@ -761,12 +714,12 @@ async fn farm_enqueue_publish_uses_explicit_idempotency_key_across_equivalent_ta
let first_receipt = sdk
.farms()
- .enqueue_publish_with_explicit_signer(first, &FixtureSigner::new(FARMER))
+ .enqueue_publish_with_explicit_signer(first, &FixtureSigner::new(farmer_pubkey()))
.await
.expect("first enqueue");
let second_receipt = sdk
.farms()
- .enqueue_publish_with_explicit_signer(second, &FixtureSigner::new(FARMER))
+ .enqueue_publish_with_explicit_signer(second, &FixtureSigner::new(farmer_pubkey()))
.await
.expect("second enqueue");
@@ -808,7 +761,7 @@ async fn farm_enqueue_publish_pushes_queued_event_with_mock_relay_sync() {
.expect("idempotency key");
let enqueue_receipt = sdk
.farms()
- .enqueue_publish_with_explicit_signer(enqueue_request, &FixtureSigner::new(FARMER))
+ .enqueue_publish_with_explicit_signer(enqueue_request, &FixtureSigner::new(farmer_pubkey()))
.await
.expect("enqueue");
let adapter = RadrootsMockRelayPublishAdapter::new();
@@ -869,7 +822,7 @@ async fn farm_multi_target_profile_publishes_after_nostr_success_and_retains_ret
)
.try_with_idempotency_key("01890f0e-6c00-7000-8000-00000000022f")
.expect("idempotency key"),
- &FixtureSigner::new(FARMER),
+ &FixtureSigner::new(farmer_pubkey()),
)
.await
.expect("enqueue");
@@ -944,7 +897,7 @@ async fn farm_enqueue_publish_reports_preflight_idempotency_conflict_without_mut
.try_with_idempotency_key("01890f0e-6c00-7000-8000-00000000022a")
.expect("idempotency key");
sdk.farms()
- .enqueue_publish_with_explicit_signer(first, &FixtureSigner::new(FARMER))
+ .enqueue_publish_with_explicit_signer(first, &FixtureSigner::new(farmer_pubkey()))
.await
.expect("first enqueue");
let paths = sdk.storage_paths().expect("paths");
@@ -980,7 +933,7 @@ async fn farm_enqueue_publish_reports_preflight_idempotency_conflict_without_mut
.expect("idempotency key");
let error = sdk
.farms()
- .enqueue_publish_with_explicit_signer(second, &FixtureSigner::new(FARMER))
+ .enqueue_publish_with_explicit_signer(second, &FixtureSigner::new(farmer_pubkey()))
.await
.expect_err("conflict");
@@ -1030,7 +983,7 @@ async fn farm_runtime_dtos_serialize_deterministically() {
prepare_json,
serde_json::json!({
"actor": {
- "pubkey": FARMER,
+ "pubkey": farmer_pubkey(),
"roles": ["farmer"],
"account_id": null,
"source": "test"
@@ -1067,7 +1020,7 @@ async fn farm_runtime_dtos_serialize_deterministically() {
enqueue_json,
serde_json::json!({
"actor": {
- "pubkey": FARMER,
+ "pubkey": farmer_pubkey(),
"roles": ["farmer"],
"account_id": null,
"source": "test"
@@ -1138,7 +1091,7 @@ async fn farm_runtime_dtos_serialize_deterministically() {
serde_json::to_value(&private_upsert).expect("private upsert json"),
serde_json::json!({
"actor": {
- "pubkey": FARMER,
+ "pubkey": farmer_pubkey(),
"roles": ["farmer"],
"account_id": null,
"source": "test"
@@ -1165,7 +1118,7 @@ async fn farm_runtime_dtos_serialize_deterministically() {
serde_json::to_value(&private_set).expect("private set json"),
serde_json::json!({
"actor": {
- "pubkey": FARMER,
+ "pubkey": farmer_pubkey(),
"roles": ["farmer"],
"account_id": null,
"source": "test"
@@ -1224,7 +1177,7 @@ async fn farm_runtime_dtos_serialize_deterministically() {
serde_json::to_value(&private_clear).expect("private clear json"),
serde_json::json!({
"actor": {
- "pubkey": FARMER,
+ "pubkey": farmer_pubkey(),
"roles": ["farmer"],
"account_id": null,
"source": "test"
@@ -1236,7 +1189,7 @@ async fn farm_runtime_dtos_serialize_deterministically() {
let private_receipt = FarmPrivateLocationReceipt {
farm_addr: farm_addr(&farmer_actor(), FARM_D_D_TAG),
- farm_pubkey: FARMER.to_owned(),
+ farm_pubkey: farmer_pubkey().to_owned(),
farm_d_tag: FARM_D_D_TAG.to_owned(),
label: Some("identifier gate".to_owned()),
exact_location: SdkExactLocation::new(48.9, -123.4),
@@ -1293,7 +1246,7 @@ async fn farm_runtime_dtos_serialize_deterministically() {
assert_struct_serialize_error_paths(&candidate, 7);
let lookup = FarmPrivateLocationLookupReceipt {
farm_addr: farm_addr(&farmer_actor(), FARM_F_D_TAG),
- farm_pubkey: FARMER.to_owned(),
+ farm_pubkey: farmer_pubkey().to_owned(),
farm_d_tag: FARM_F_D_TAG.to_owned(),
input: FarmPrivateLocationInput::query("Shared Market"),
candidates: vec![candidate],
@@ -1322,7 +1275,7 @@ async fn farm_runtime_dtos_serialize_deterministically() {
let receipt = sdk
.farms()
- .enqueue_publish_with_explicit_signer(enqueue_request, &FixtureSigner::new(FARMER))
+ .enqueue_publish_with_explicit_signer(enqueue_request, &FixtureSigner::new(farmer_pubkey()))
.await
.expect("enqueue");
let receipt_json = serde_json::to_value(&receipt).expect("receipt json");
diff --git a/crates/sdk/tests/knowledge_public_api.rs b/crates/sdk/tests/knowledge_public_api.rs
@@ -3,8 +3,9 @@
use nostr::{EventBuilder, Keys, Kind, Tag, Timestamp};
use radroots_event::RadrootsEventEnvelopeParts;
use radroots_sdk::knowledge::prelude::*;
+use std::sync::LazyLock;
-const SECRET_KEY_HEX: &str = "0101010101010101010101010101010101010101010101010101010101010101";
+static KNOWLEDGE_KEYS: LazyLock<Keys> = LazyLock::new(Keys::generate);
const CREATED_AT: u32 = 1_800_000_000;
const RELAY: &str = "wss://relay.radroots.example";
@@ -410,6 +411,35 @@ fn knowledge_errors_expose_stable_codes() {
.into();
assert_eq!(draft_error.code(), "knowledge_draft");
assert_eq!(draft_error.inner_code(), "missing_tag");
+
+ let draft_errors = [
+ (
+ RadrootsDraftError::ContractNotDraftAuthorable {
+ contract_id: KNOWLEDGE_CLAIM_CONTRACT_ID.to_owned(),
+ },
+ "contract_not_draft_authorable",
+ ),
+ (
+ RadrootsDraftError::ContractRegistryVersionMismatch {
+ expected: 2,
+ actual: 1,
+ },
+ "contract_registry_version_mismatch",
+ ),
+ (
+ RadrootsDraftError::DraftExpectedEventIdMismatch {
+ expected_event_id: hex_64('a'),
+ actual_event_id: hex_64('b'),
+ },
+ "draft_expected_event_id_mismatch",
+ ),
+ ];
+
+ for (draft_error, expected_inner_code) in draft_errors {
+ let error = RadrootsSdkKnowledgeError::from(draft_error);
+ assert_eq!(error.code(), "knowledge_draft");
+ assert_eq!(error.inner_code(), expected_inner_code);
+ }
}
fn sign_parts(parts: RadrootsNip01EventWireParts) -> RadrootsEventEnvelope {
@@ -419,11 +449,10 @@ fn sign_parts(parts: RadrootsNip01EventWireParts) -> RadrootsEventEnvelope {
.map(Tag::parse)
.collect::<Result<Vec<_>, _>>()
.expect("tags");
- let keys = Keys::parse(SECRET_KEY_HEX).expect("keys");
let event = EventBuilder::new(Kind::Custom(parts.kind as u16), parts.content)
.tags(tags)
.custom_created_at(Timestamp::from_secs(u64::from(CREATED_AT)))
- .sign_with_keys(&keys)
+ .sign_with_keys(&KNOWLEDGE_KEYS)
.expect("signed event");
RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts {
id: event.id.to_hex(),
@@ -443,10 +472,7 @@ fn sign_parts(parts: RadrootsNip01EventWireParts) -> RadrootsEventEnvelope {
}
fn public_key_hex() -> String {
- Keys::parse(SECRET_KEY_HEX)
- .expect("keys")
- .public_key()
- .to_hex()
+ KNOWLEDGE_KEYS.public_key().to_hex()
}
fn hex_64(character: char) -> String {
diff --git a/crates/sdk/tests/listings_runtime.rs b/crates/sdk/tests/listings_runtime.rs
@@ -1,19 +1,21 @@
#![cfg(feature = "runtime")]
-use radroots_authority::{
- RadrootsActorContext, RadrootsEventSigner, RadrootsSignerError, RadrootsSignerIdentity,
-};
+use radroots_authority::RadrootsActorContext;
use radroots_core::{
RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreQuantity,
RadrootsCoreQuantityPrice, RadrootsCoreUnit,
};
use radroots_event::{
contract::RadrootsActorRole,
- draft::{RadrootsEventDraft, RadrootsSignedEvent, RadrootsSignedEventParts},
farm::RadrootsFarmRef,
ids::{RadrootsDTag, RadrootsInventoryBinId},
- kinds::KIND_LISTING,
- listing::{RadrootsListing, RadrootsListingBin, RadrootsListingProduct},
+ kinds::KIND_CLASSIFIED_LISTING,
+ operational_listing::{
+ RadrootsOperationalListing, RadrootsOperationalListingAvailability,
+ RadrootsOperationalListingBin, RadrootsOperationalListingDeliveryMethod,
+ RadrootsOperationalListingProduct, RadrootsOperationalListingPublicLocation,
+ RadrootsOperationalListingStatus,
+ },
};
use radroots_event_store::RadrootsEventStore;
use radroots_outbox::{
@@ -27,17 +29,18 @@ use radroots_sdk::{
RadrootsSdkTimestamp, ReticulumProfile, SdkIdempotencyKey, SdkMutationState, TargetPolicy,
TargetSet, TransportProfile,
};
-use radroots_trade::listing::RadrootsListingEditDocumentV1;
+use radroots_trade::operational_listing::RadrootsOperationalListingEditDocumentV1;
use radroots_transport_nostr::{RadrootsMockRelayPublishAdapter, RadrootsNostrTransport};
use sqlx::Row;
+#[path = "support/fixture_signer.rs"]
+mod fixture_signer;
#[path = "support/serializer_failure.rs"]
mod serializer_failure;
+use fixture_signer::{FixtureSigner, fixture_alice_pubkey, fixture_bob_pubkey};
use serializer_failure::assert_struct_serialize_error_paths;
-const SELLER: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
-const OTHER: &str = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
const FARM_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAAAA";
const LISTING_A_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAAAQ";
const LISTING_B_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAAAg";
@@ -53,77 +56,31 @@ const LISTING_K_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAACw";
const RELAY: &str = "wss://relay.example.com";
const RELAY_B: &str = "wss://relay-b.example.com";
-#[derive(Clone)]
-struct FixtureSigner {
- identity: RadrootsSignerIdentity,
+fn seller_pubkey() -> &'static str {
+ fixture_alice_pubkey()
}
-impl FixtureSigner {
- fn new(pubkey: &str) -> Self {
- Self {
- identity: RadrootsSignerIdentity::new(pubkey).expect("identity"),
- }
- }
-}
-
-impl RadrootsEventSigner for FixtureSigner {
- fn pubkey(&self) -> &radroots_event::ids::RadrootsPublicKey {
- self.identity.pubkey()
- }
-
- fn sign_frozen_draft(
- &self,
- draft: &RadrootsEventDraft,
- ) -> Result<RadrootsSignedEvent, RadrootsSignerError> {
- if self.pubkey().as_str() != draft.expected_pubkey_str() {
- return Err(RadrootsSignerError::SigningFailed {
- message: "wrong fixture signer".to_owned(),
- });
- }
- let sig = "f".repeat(128);
- let raw_json = serde_json::json!({
- "id": draft.expected_event_id_str(),
- "pubkey": self.pubkey().as_str(),
- "created_at": draft.created_at_u64(),
- "kind": draft.kind_u32(),
- "tags": draft.tags_as_vec(),
- "content": draft.content(),
- "sig": sig,
- })
- .to_string();
- RadrootsSignedEvent::new(RadrootsSignedEventParts {
- id: draft.expected_event_id_str().to_owned(),
- pubkey: self.pubkey().as_str().to_owned(),
- created_at: draft.created_at_u64(),
- kind: draft.kind_u32(),
- tags: draft.tags_as_vec(),
- content: draft.content().to_owned(),
- sig,
- raw_json,
- })
- .map_err(|error| RadrootsSignerError::SigningFailed {
- message: error.to_string(),
- })
- }
+fn other_pubkey() -> &'static str {
+ fixture_bob_pubkey()
}
fn actor() -> RadrootsActorContext {
- RadrootsActorContext::test(SELLER, [RadrootsActorRole::Seller]).expect("actor")
+ RadrootsActorContext::test(seller_pubkey(), [RadrootsActorRole::Seller]).expect("actor")
}
fn non_seller_actor() -> RadrootsActorContext {
- RadrootsActorContext::test(SELLER, [RadrootsActorRole::Buyer]).expect("actor")
+ RadrootsActorContext::test(seller_pubkey(), [RadrootsActorRole::Buyer]).expect("actor")
}
-fn listing(d_tag: &str, title: &str) -> RadrootsListing {
- RadrootsListing {
+fn listing(d_tag: &str, title: &str) -> RadrootsOperationalListing {
+ RadrootsOperationalListing {
d_tag: RadrootsDTag::parse(d_tag).expect("d tag"),
published_at: None,
farm: RadrootsFarmRef {
- pubkey: SELLER.to_owned(),
+ pubkey: seller_pubkey().to_owned(),
d_tag: FARM_D_TAG.to_owned(),
},
- product: RadrootsListingProduct {
+ product: RadrootsOperationalListingProduct {
key: "coffee".to_owned(),
title: title.to_owned(),
category: "coffee".to_owned(),
@@ -135,7 +92,7 @@ fn listing(d_tag: &str, title: &str) -> RadrootsListing {
year: None,
},
primary_bin_id: RadrootsInventoryBinId::parse("bin-1").expect("bin id"),
- bins: vec![RadrootsListingBin {
+ bins: vec![RadrootsOperationalListingBin {
bin_id: RadrootsInventoryBinId::parse("bin-1").expect("bin id"),
quantity: RadrootsCoreQuantity::new(
RadrootsCoreDecimal::from(1000u32),
@@ -160,10 +117,18 @@ fn listing(d_tag: &str, title: &str) -> RadrootsListing {
resource_area: None,
plot: None,
discounts: None,
- inventory_available: None,
- availability: None,
- delivery_method: None,
- location: None,
+ inventory_available: Some(RadrootsCoreDecimal::from(5u32)),
+ availability: Some(RadrootsOperationalListingAvailability::Status {
+ status: RadrootsOperationalListingStatus::Active,
+ }),
+ delivery_method: Some(RadrootsOperationalListingDeliveryMethod::Pickup),
+ location: Some(RadrootsOperationalListingPublicLocation {
+ primary: "Victoria".to_owned(),
+ city: Some("Victoria".to_owned()),
+ region: Some("British Columbia".to_owned()),
+ country: Some("CA".to_owned()),
+ geohash: "c287g".to_owned(),
+ }),
images: None,
}
}
@@ -208,15 +173,18 @@ async fn prepare_publish_is_side_effect_free() {
let request = ListingPreparePublishRequest::new(actor(), listing(LISTING_A_D_TAG, "Coffee"));
let prepared = sdk.listings().prepare_publish(request).expect("prepared");
- assert_eq!(prepared.frozen_draft.kind_u32(), KIND_LISTING);
- assert_eq!(prepared.created_at.unix_seconds(), 1_700_000_000);
+ assert_eq!(prepared.frozen_draft().kind_u32(), KIND_CLASSIFIED_LISTING);
+ assert_eq!(prepared.created_at().unix_seconds(), 1_700_000_000);
assert_eq!(
- prepared.expected_event_id,
- prepared.frozen_draft.expected_event_id_str()
+ prepared.expected_event_id().as_str(),
+ prepared.frozen_draft().expected_event_id_str()
);
assert_eq!(
- prepared.public_listing_addr.as_str(),
- format!("{KIND_LISTING}:{SELLER}:{LISTING_A_D_TAG}")
+ prepared.public_listing_addr().as_str(),
+ format!(
+ "{KIND_CLASSIFIED_LISTING}:{}:{LISTING_A_D_TAG}",
+ seller_pubkey()
+ )
);
let paths = sdk.storage_paths().expect("paths");
@@ -225,7 +193,7 @@ async fn prepare_publish_is_side_effect_free() {
.expect("event store");
assert!(
event_store
- .get_event(prepared.expected_event_id.as_str())
+ .raw_event(prepared.expected_event_id().as_str())
.await
.expect("event lookup")
.is_none()
@@ -275,13 +243,13 @@ async fn enqueue_publish_stores_event_and_queues_signed_outbox_without_publish()
.expect("prepared");
let receipt = sdk
.listings()
- .enqueue_publish_with_explicit_signer(request, &FixtureSigner::new(SELLER))
+ .enqueue_publish_with_explicit_signer(request, &FixtureSigner::new(seller_pubkey()))
.await
.expect("enqueue");
- assert_eq!(receipt.expected_event_id, prepared.expected_event_id);
+ assert_eq!(&receipt.expected_event_id, prepared.expected_event_id());
assert_eq!(receipt.signed_event_id, receipt.expected_event_id);
- assert_eq!(receipt.public_listing_addr, prepared.public_listing_addr);
+ assert_eq!(&receipt.public_listing_addr, prepared.public_listing_addr());
assert_eq!(receipt.local_event_seq, 1);
assert_eq!(receipt.outbox_operation_id, 1);
assert_eq!(receipt.outbox_event_id, 1);
@@ -294,7 +262,7 @@ async fn enqueue_publish_stores_event_and_queues_signed_outbox_without_publish()
.expect("event store");
assert!(
event_store
- .get_event(receipt.signed_event_id.as_str())
+ .valid_event(receipt.signed_event_id.as_str())
.await
.expect("event lookup")
.is_some()
@@ -323,7 +291,7 @@ async fn enqueue_publish_default_profile_rejects_empty_transport_targets() {
let error = sdk
.listings()
- .enqueue_publish_with_explicit_signer(request, &FixtureSigner::new(SELLER))
+ .enqueue_publish_with_explicit_signer(request, &FixtureSigner::new(seller_pubkey()))
.await
.expect_err("empty transport profile");
@@ -355,13 +323,13 @@ async fn prepare_then_enqueue_prepared_uses_same_event_id() {
SdkIdempotencyKey::new("01890f0e-6c00-7000-8000-000000000238")
.expect("idempotency"),
),
- &FixtureSigner::new(SELLER),
+ &FixtureSigner::new(seller_pubkey()),
)
.await
.expect("prepared enqueue");
- assert_eq!(receipt.expected_event_id, prepared.expected_event_id);
- assert_eq!(receipt.signed_event_id, prepared.expected_event_id);
+ assert_eq!(&receipt.expected_event_id, prepared.expected_event_id());
+ assert_eq!(&receipt.signed_event_id, prepared.expected_event_id());
let paths = sdk.storage_paths().expect("paths");
let event_store = RadrootsEventStore::open_file(&paths.runtime_path)
@@ -369,7 +337,7 @@ async fn prepare_then_enqueue_prepared_uses_same_event_id() {
.expect("event store");
assert!(
event_store
- .get_event(prepared.expected_event_id.as_str())
+ .valid_event(prepared.expected_event_id().as_str())
.await
.expect("event lookup")
.is_some()
@@ -383,7 +351,7 @@ async fn prepare_then_enqueue_prepared_uses_same_event_id() {
.await
.expect("outbox event")
.expect("outbox event");
- assert_eq!(outbox_event.event_id, prepared.expected_event_id.as_str());
+ assert_eq!(outbox_event.event_id, prepared.expected_event_id().as_str());
}
#[tokio::test]
@@ -398,7 +366,7 @@ async fn enqueue_receipt_debug_omits_signed_event_payload_material() {
.expect("idempotency key");
let receipt = sdk
.listings()
- .enqueue_publish_with_explicit_signer(request, &FixtureSigner::new(SELLER))
+ .enqueue_publish_with_explicit_signer(request, &FixtureSigner::new(seller_pubkey()))
.await
.expect("enqueue");
let debug = format!("{receipt:?}");
@@ -430,13 +398,16 @@ async fn listing_runtime_dtos_serialize_deterministically() {
let created_at = RadrootsSdkTimestamp::from_unix_seconds(1_700_000_123);
let prepare_request = ListingPreparePublishRequest::from_document(
actor(),
- RadrootsListingEditDocumentV1::new(listing(LISTING_A_D_TAG, "Serialized Coffee")),
+ RadrootsOperationalListingEditDocumentV1::new(listing(
+ LISTING_A_D_TAG,
+ "Serialized Coffee",
+ )),
)
.with_created_at(created_at);
let prepare_json = serde_json::to_value(&prepare_request).expect("prepare request json");
assert_struct_serialize_error_paths(&prepare_request, 3);
- assert_eq!(prepare_json["actor"]["pubkey"], SELLER);
+ assert_eq!(prepare_json["actor"]["pubkey"], seller_pubkey());
assert_eq!(
prepare_json["actor"]["roles"],
serde_json::json!(["seller"])
@@ -501,7 +472,7 @@ async fn listing_runtime_dtos_serialize_deterministically() {
let try_key_request = ListingEnqueuePublishRequest::from_document(
actor(),
- RadrootsListingEditDocumentV1::new(listing(LISTING_C_D_TAG, "Queued Coffee")),
+ RadrootsOperationalListingEditDocumentV1::new(listing(LISTING_C_D_TAG, "Queued Coffee")),
TargetPolicy::default_profile(),
)
.try_with_idempotency_key("01890f0e-6c00-7000-8000-000000000227")
@@ -513,7 +484,7 @@ async fn listing_runtime_dtos_serialize_deterministically() {
let receipt = sdk
.listings()
- .enqueue_publish_with_explicit_signer(enqueue_request, &FixtureSigner::new(SELLER))
+ .enqueue_publish_with_explicit_signer(enqueue_request, &FixtureSigner::new(seller_pubkey()))
.await
.expect("enqueue");
let receipt_json = serde_json::to_value(&receipt).expect("receipt json");
@@ -544,7 +515,7 @@ async fn enqueue_publish_convenience_matches_prepare_plus_enqueue_prepared() {
SdkIdempotencyKey::new("01890f0e-6c00-7000-8000-000000000239")
.expect("idempotency"),
),
- &FixtureSigner::new(SELLER),
+ &FixtureSigner::new(seller_pubkey()),
)
.await
.expect("prepared enqueue");
@@ -559,7 +530,10 @@ async fn enqueue_publish_convenience_matches_prepare_plus_enqueue_prepared() {
.expect("idempotency");
let convenience_receipt = convenience_sdk
.listings()
- .enqueue_publish_with_explicit_signer(convenience_request, &FixtureSigner::new(SELLER))
+ .enqueue_publish_with_explicit_signer(
+ convenience_request,
+ &FixtureSigner::new(seller_pubkey()),
+ )
.await
.expect("convenience enqueue");
@@ -586,7 +560,7 @@ async fn enqueue_prepared_publish_returns_structured_actor_errors() {
SdkIdempotencyKey::new("01890f0e-6c00-7000-8000-00000000023e")
.expect("idempotency key"),
),
- &FixtureSigner::new(SELLER),
+ &FixtureSigner::new(seller_pubkey()),
)
.await
.expect_err("actor error");
@@ -615,7 +589,7 @@ async fn enqueue_prepared_publish_returns_sanitized_signer_errors() {
SdkIdempotencyKey::new("01890f0e-6c00-7000-8000-00000000023f")
.expect("idempotency key"),
),
- &FixtureSigner::new(OTHER),
+ &FixtureSigner::new(other_pubkey()),
)
.await
.expect_err("signer error");
@@ -645,7 +619,7 @@ async fn explicit_historical_created_at_does_not_backdate_observed_at_ms() {
let receipt = sdk
.listings()
- .enqueue_publish_with_explicit_signer(request, &FixtureSigner::new(SELLER))
+ .enqueue_publish_with_explicit_signer(request, &FixtureSigner::new(seller_pubkey()))
.await
.expect("enqueue");
@@ -654,10 +628,11 @@ async fn explicit_historical_created_at_does_not_backdate_observed_at_ms() {
.await
.expect("event store");
let stored_event = event_store
- .get_event(receipt.signed_event_id.as_str())
+ .valid_event(receipt.signed_event_id.as_str())
.await
.expect("event lookup")
.expect("stored event");
+ let stored_event = stored_event.raw_event();
assert_eq!(stored_event.created_at, 1_600_000_000);
assert_eq!(stored_event.inserted_at_ms, observed_at_ms);
assert_eq!(stored_event.updated_at_ms, observed_at_ms);
@@ -691,7 +666,7 @@ async fn enqueue_publish_returns_sanitized_signer_errors() {
.expect("idempotency key");
let error = sdk
.listings()
- .enqueue_publish_with_explicit_signer(request, &FixtureSigner::new(OTHER))
+ .enqueue_publish_with_explicit_signer(request, &FixtureSigner::new(other_pubkey()))
.await
.expect_err("signer error");
let message = error.to_string();
@@ -715,7 +690,7 @@ async fn enqueue_publish_reports_preflight_idempotency_conflict_without_mutation
.try_with_idempotency_key("01890f0e-6c00-7000-8000-000000000228")
.expect("idempotency key");
sdk.listings()
- .enqueue_publish_with_explicit_signer(first, &FixtureSigner::new(SELLER))
+ .enqueue_publish_with_explicit_signer(first, &FixtureSigner::new(seller_pubkey()))
.await
.expect("first enqueue");
let paths = sdk.storage_paths().expect("paths");
@@ -751,7 +726,7 @@ async fn enqueue_publish_reports_preflight_idempotency_conflict_without_mutation
.expect("idempotency key");
let error = sdk
.listings()
- .enqueue_publish_with_explicit_signer(second, &FixtureSigner::new(SELLER))
+ .enqueue_publish_with_explicit_signer(second, &FixtureSigner::new(seller_pubkey()))
.await
.expect_err("conflict");
@@ -799,7 +774,7 @@ async fn enqueue_publish_rolls_back_event_and_journal_when_outbox_conflicts_afte
"INSERT INTO outbox_operations(operation_kind, expected_pubkey, semantic_scope, trade_id, mutation_id, canonical_payload_sha256, idempotency_key, operation_idempotency_digest, status, created_at_ms, updated_at_ms) VALUES (?, ?, 'generic_event', NULL, NULL, NULL, ?, ?, 'queued', ?, ?)",
)
.bind(LISTING_PUBLISH_OPERATION_KIND)
- .bind(SELLER)
+ .bind(seller_pubkey())
.bind(idempotency_key)
.bind("conflicting-digest")
.bind(1_700_000_000_000i64)
@@ -813,7 +788,7 @@ async fn enqueue_publish_rolls_back_event_and_journal_when_outbox_conflicts_afte
)
.bind(conflicting_operation)
.bind("0".repeat(64))
- .bind(SELLER)
+ .bind(seller_pubkey())
.bind(1_700_000_000_000i64)
.bind(1_700_000_000_000i64)
.bind(1_700_000_000_000i64)
@@ -837,7 +812,7 @@ async fn enqueue_publish_rolls_back_event_and_journal_when_outbox_conflicts_afte
.expect("prepared");
let error = sdk
.listings()
- .enqueue_publish_with_explicit_signer(request, &FixtureSigner::new(SELLER))
+ .enqueue_publish_with_explicit_signer(request, &FixtureSigner::new(seller_pubkey()))
.await
.expect_err("outbox conflict");
assert!(matches!(
@@ -850,7 +825,7 @@ async fn enqueue_publish_rolls_back_event_and_journal_when_outbox_conflicts_afte
.expect("event store");
assert!(
event_store
- .get_event(prepared.expected_event_id.as_str())
+ .raw_event(prepared.expected_event_id().as_str())
.await
.expect("event lookup")
.is_none()
@@ -859,7 +834,7 @@ async fn enqueue_publish_rolls_back_event_and_journal_when_outbox_conflicts_afte
"SELECT state, last_error_code FROM sdk_runtime_operation_journal WHERE operation_kind = ? AND actor_pubkey = ? AND idempotency_key = ?",
)
.bind(LISTING_PUBLISH_OPERATION_KIND)
- .bind(SELLER)
+ .bind(seller_pubkey())
.bind(idempotency_key)
.fetch_one(event_store.pool())
.await
@@ -874,7 +849,7 @@ async fn enqueue_publish_rolls_back_event_and_journal_when_outbox_conflicts_afte
"SELECT COUNT(*) FROM sdk_runtime_recovery_receipt WHERE recovery_code = 'idempotency_conflict' AND operation_kind = ? AND actor_pubkey = ? AND idempotency_key = ?",
)
.bind(LISTING_PUBLISH_OPERATION_KIND)
- .bind(SELLER)
+ .bind(seller_pubkey())
.bind(idempotency_key)
.fetch_one(event_store.pool())
.await
@@ -920,12 +895,12 @@ async fn enqueue_publish_uses_explicit_idempotency_key_across_equivalent_target_
let first_receipt = sdk
.listings()
- .enqueue_publish_with_explicit_signer(first, &FixtureSigner::new(SELLER))
+ .enqueue_publish_with_explicit_signer(first, &FixtureSigner::new(seller_pubkey()))
.await
.expect("first enqueue");
let second_receipt = sdk
.listings()
- .enqueue_publish_with_explicit_signer(second, &FixtureSigner::new(SELLER))
+ .enqueue_publish_with_explicit_signer(second, &FixtureSigner::new(seller_pubkey()))
.await
.expect("second enqueue");
@@ -966,7 +941,7 @@ async fn listing_multi_target_profile_publishes_after_nostr_success_and_retains_
)
.try_with_idempotency_key("01890f0e-6c00-7000-8000-00000000023c")
.expect("idempotency"),
- &FixtureSigner::new(SELLER),
+ &FixtureSigner::new(seller_pubkey()),
)
.await
.expect("enqueue");
diff --git a/crates/sdk/tests/runtime_foundation.rs b/crates/sdk/tests/runtime_foundation.rs
@@ -3,9 +3,9 @@
use radroots_sdk::{
BackupRequest, IntegrityRequest, LISTING_PUBLISH_OPERATION_KIND, NostrProfile,
NostrRelayUrlPolicy, RadrootsClient, RadrootsSdkClock, RadrootsSdkError, RadrootsSdkErrorClass,
- RadrootsSdkGeoNamesErrorKind, RadrootsSdkRecoveryAction, RadrootsSdkStorageConfig,
- RadrootsSdkTimestamp, RadrootsSdkTradeErrorKind, RestoreRequest, ReticulumBehavior,
- SDK_IDEMPOTENCY_KEY_MAX_LEN, SDK_TRANSPORT_TARGET_MAX_COUNT, SdkBackupState,
+ RadrootsSdkGeoNamesErrorKind, RadrootsSdkListingValidationErrorKind, RadrootsSdkRecoveryAction,
+ RadrootsSdkStorageConfig, RadrootsSdkTimestamp, RadrootsSdkTradeErrorKind, RestoreRequest,
+ ReticulumBehavior, SDK_IDEMPOTENCY_KEY_MAX_LEN, SDK_TRANSPORT_TARGET_MAX_COUNT, SdkBackupState,
SdkBackupVerification, SdkEventStoreStorageStatus, SdkIdempotencyKey, SdkOutboxStorageStatus,
SdkPrivateStoreStorageStatus, SdkRestoreState, SdkSqliteStoreStatus,
SdkSqliteWalCheckpointReceipt, SdkSqliteWalStatus, SdkStorageKind, SdkStudioStoreStorageStatus,
@@ -539,6 +539,16 @@ fn sdk_error_contract_methods_cover_all_variants() {
vec![RadrootsSdkRecoveryAction::FixRequest],
),
(
+ RadrootsSdkError::ListingValidation {
+ kind: RadrootsSdkListingValidationErrorKind::MissingInventory,
+ message: "missing listing inventory".to_owned(),
+ },
+ "listing_validation",
+ RadrootsSdkErrorClass::Request,
+ false,
+ vec![RadrootsSdkRecoveryAction::FixRequest],
+ ),
+ (
RadrootsSdkError::ListingMutation {
message: "mutation".to_owned(),
},
@@ -753,7 +763,7 @@ fn storage_backup_and_integrity_contract_dtos_serialize() {
event_store: SdkEventStoreStorageStatus {
store: store.clone(),
total_events: 2,
- projection_eligible_events: 1,
+ valid_stream_events: 1,
transport_observations: 1,
last_event_seq: Some(2),
last_event_updated_at_ms: Some(1_700_000_000_000),
@@ -797,7 +807,7 @@ fn storage_backup_and_integrity_contract_dtos_serialize() {
"integrity_result": "ok"
},
"total_events": 2,
- "projection_eligible_events": 1,
+ "valid_stream_events": 1,
"transport_observations": 1,
"last_event_seq": 2,
"last_event_updated_at_ms": 1700000000000i64
@@ -856,6 +866,33 @@ fn storage_backup_and_integrity_contract_dtos_serialize() {
}
})
);
+ let mut legacy_event_store_json = serde_json::to_value(SdkEventStoreStorageStatus {
+ store: store.clone(),
+ total_events: 2,
+ valid_stream_events: 1,
+ transport_observations: 1,
+ last_event_seq: Some(2),
+ last_event_updated_at_ms: Some(1_700_000_000_000),
+ })
+ .expect("event store status");
+ let legacy_event_store_object = legacy_event_store_json
+ .as_object_mut()
+ .expect("event store status object");
+ let valid_stream_events = legacy_event_store_object
+ .remove("valid_stream_events")
+ .expect("valid stream events");
+ legacy_event_store_object.insert("projection_eligible_events".to_owned(), valid_stream_events);
+ let legacy_event_store: SdkEventStoreStorageStatus =
+ serde_json::from_value(legacy_event_store_json).expect("legacy event store status");
+ assert_eq!(legacy_event_store.valid_stream_events, 1);
+ let current_event_store_json =
+ serde_json::to_value(legacy_event_store).expect("current event store status");
+ assert_eq!(current_event_store_json["valid_stream_events"], 1);
+ assert!(
+ current_event_store_json
+ .get("projection_eligible_events")
+ .is_none()
+ );
assert_eq!(
serde_json::to_value(StorageCheckpointRequest::new()).expect("checkpoint request"),
serde_json::json!({})
diff --git a/crates/sdk/tests/source_boundary.rs b/crates/sdk/tests/source_boundary.rs
@@ -12,12 +12,24 @@ fn read_source(path: &Path) -> String {
}
fn active_source_files() -> Vec<PathBuf> {
- let src = manifest_dir().join("src");
- fs::read_dir(src)
- .expect("src entries")
- .map(|entry| entry.expect("src entry").path())
- .filter(|path| path.extension().is_some_and(|extension| extension == "rs"))
- .collect()
+ fn collect_rust_sources(directory: &Path, files: &mut Vec<PathBuf>) {
+ let mut entries = fs::read_dir(directory)
+ .unwrap_or_else(|error| panic!("read {}: {error}", directory.display()))
+ .map(|entry| entry.expect("source entry").path())
+ .collect::<Vec<_>>();
+ entries.sort();
+ for path in entries {
+ if path.is_dir() {
+ collect_rust_sources(&path, files);
+ } else if path.extension().is_some_and(|extension| extension == "rs") {
+ files.push(path);
+ }
+ }
+ }
+
+ let mut files = Vec::new();
+ collect_rust_sources(&manifest_dir().join("src"), &mut files);
+ files
}
#[test]
@@ -72,6 +84,25 @@ fn active_sources_do_not_import_retired_trade_modules() {
}
#[test]
+fn active_sources_do_not_import_retired_listing_contracts() {
+ for path in active_source_files() {
+ let source = read_source(&path);
+ for forbidden in [
+ "radroots_event::listing",
+ "radroots_trade::listing",
+ "RadrootsListingAddress",
+ "KIND_LISTING",
+ ] {
+ assert!(
+ !source.contains(forbidden),
+ "{} must not import retired listing contract `{forbidden}`",
+ path.display()
+ );
+ }
+ }
+}
+
+#[test]
fn active_sources_do_not_describe_compatibility_paths() {
for path in active_source_files() {
let source = read_source(&path).to_lowercase();
diff --git a/crates/sdk/tests/support/fixture_signer.rs b/crates/sdk/tests/support/fixture_signer.rs
@@ -1,15 +1,48 @@
use radroots_authority::{RadrootsEventSigner, RadrootsSignerError, RadrootsSignerIdentity};
-use radroots_event::draft::{RadrootsEventDraft, RadrootsSignedEvent, RadrootsSignedEventParts};
+use radroots_event::draft::{RadrootsEventDraft, RadrootsSignedEvent};
+use radroots_nostr::prelude::{RadrootsNostrKeys, radroots_nostr_sign_frozen_draft};
+use std::sync::LazyLock;
+
+struct FixtureKeyMaterial {
+ keys: RadrootsNostrKeys,
+ pubkey: String,
+}
+
+impl FixtureKeyMaterial {
+ fn generate() -> Self {
+ let keys = RadrootsNostrKeys::generate();
+ let pubkey = keys.public_key().to_hex();
+ Self { keys, pubkey }
+ }
+}
+
+static FIXTURE_ALICE: LazyLock<FixtureKeyMaterial> = LazyLock::new(FixtureKeyMaterial::generate);
+static FIXTURE_BOB: LazyLock<FixtureKeyMaterial> = LazyLock::new(FixtureKeyMaterial::generate);
+
+pub(crate) fn fixture_alice_pubkey() -> &'static str {
+ FIXTURE_ALICE.pubkey.as_str()
+}
+
+pub(crate) fn fixture_bob_pubkey() -> &'static str {
+ FIXTURE_BOB.pubkey.as_str()
+}
#[derive(Clone)]
pub struct FixtureSigner {
identity: RadrootsSignerIdentity,
+ keys: RadrootsNostrKeys,
}
impl FixtureSigner {
pub fn new(pubkey: &str) -> Self {
+ let material = match pubkey {
+ pubkey if pubkey == fixture_alice_pubkey() => &*FIXTURE_ALICE,
+ pubkey if pubkey == fixture_bob_pubkey() => &*FIXTURE_BOB,
+ _ => panic!("unsupported fixture signer public key"),
+ };
Self {
identity: RadrootsSignerIdentity::new(pubkey).expect("identity"),
+ keys: material.keys.clone(),
}
}
}
@@ -28,29 +61,10 @@ impl RadrootsEventSigner for FixtureSigner {
message: "wrong fixture signer".to_owned(),
});
}
- let sig = "f".repeat(128);
- let raw_json = serde_json::json!({
- "id": draft.expected_event_id_str(),
- "pubkey": self.pubkey().as_str(),
- "created_at": draft.created_at_u64(),
- "kind": draft.kind_u32(),
- "tags": draft.tags_as_vec(),
- "content": draft.content(),
- "sig": sig,
- })
- .to_string();
- RadrootsSignedEvent::new(RadrootsSignedEventParts {
- id: draft.expected_event_id_str().to_owned(),
- pubkey: self.pubkey().as_str().to_owned(),
- created_at: draft.created_at_u64(),
- kind: draft.kind_u32(),
- tags: draft.tags_as_vec(),
- content: draft.content().to_owned(),
- sig,
- raw_json,
- })
- .map_err(|error| RadrootsSignerError::SigningFailed {
- message: error.to_string(),
+ radroots_nostr_sign_frozen_draft(&self.keys, draft).map_err(|error| {
+ RadrootsSignerError::SigningFailed {
+ message: error.to_string(),
+ }
})
}
}
diff --git a/crates/sdk/tests/sync_runtime.rs b/crates/sdk/tests/sync_runtime.rs
@@ -1,19 +1,21 @@
#![cfg(feature = "runtime")]
use futures::future::BoxFuture;
-use radroots_authority::{
- RadrootsActorContext, RadrootsEventSigner, RadrootsSignerError, RadrootsSignerIdentity,
-};
+use radroots_authority::{RadrootsActorContext, RadrootsEventSigner};
use radroots_core::{
RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreQuantity,
RadrootsCoreQuantityPrice, RadrootsCoreUnit,
};
use radroots_event::{
contract::RadrootsActorRole,
- draft::{RadrootsEventDraft, RadrootsSignedEvent, RadrootsSignedEventParts},
farm::RadrootsFarmRef,
ids::{RadrootsDTag, RadrootsEventId, RadrootsInventoryBinId},
- listing::{RadrootsListing, RadrootsListingBin, RadrootsListingProduct},
+ operational_listing::{
+ RadrootsOperationalListing, RadrootsOperationalListingAvailability,
+ RadrootsOperationalListingBin, RadrootsOperationalListingDeliveryMethod,
+ RadrootsOperationalListingProduct, RadrootsOperationalListingPublicLocation,
+ RadrootsOperationalListingStatus,
+ },
};
use radroots_event_store::{RadrootsEventStore, RadrootsTransportObservationType};
use radroots_outbox::{
@@ -52,7 +54,11 @@ use std::sync::{Arc, Mutex};
use std::thread::JoinHandle;
use std::time::Duration;
-const SELLER: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
+#[path = "support/fixture_signer.rs"]
+mod fixture_signer;
+
+use fixture_signer::{FixtureSigner, fixture_alice_pubkey};
+
const FARM_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAAAA";
const LISTING_A_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAAAQ";
const LISTING_B_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAAAg";
@@ -68,9 +74,8 @@ const PRIVATE_LAN_WS_RELAY: &str = "ws://192.168.1.10:8080";
#[cfg(feature = "radrootsd-execution")]
const RADROOTSD_EXECUTION_TEST_RELAY: &str = "wss://daemon-resolved.example.com";
-#[derive(Clone)]
-struct FixtureSigner {
- identity: RadrootsSignerIdentity,
+fn seller_pubkey() -> &'static str {
+ fixture_alice_pubkey()
}
struct TransportFailurePublishAdapter;
@@ -382,68 +387,19 @@ impl RadrootsRelayPublishAdapter for RecordingPublishAdapter {
}
}
-impl FixtureSigner {
- fn new(pubkey: &str) -> Self {
- Self {
- identity: RadrootsSignerIdentity::new(pubkey).expect("identity"),
- }
- }
-}
-
-impl RadrootsEventSigner for FixtureSigner {
- fn pubkey(&self) -> &radroots_event::ids::RadrootsPublicKey {
- self.identity.pubkey()
- }
-
- fn sign_frozen_draft(
- &self,
- draft: &RadrootsEventDraft,
- ) -> Result<RadrootsSignedEvent, RadrootsSignerError> {
- if self.pubkey().as_str() != draft.expected_pubkey_str() {
- return Err(RadrootsSignerError::SigningFailed {
- message: "wrong fixture signer".to_owned(),
- });
- }
- let sig = "f".repeat(128);
- let raw_json = serde_json::json!({
- "id": draft.expected_event_id_str(),
- "pubkey": self.pubkey().as_str(),
- "created_at": draft.created_at_u64(),
- "kind": draft.kind_u32(),
- "tags": draft.tags_as_vec(),
- "content": draft.content(),
- "sig": sig,
- })
- .to_string();
- RadrootsSignedEvent::new(RadrootsSignedEventParts {
- id: draft.expected_event_id_str().to_owned(),
- pubkey: self.pubkey().as_str().to_owned(),
- created_at: draft.created_at_u64(),
- kind: draft.kind_u32(),
- tags: draft.tags_as_vec(),
- content: draft.content().to_owned(),
- sig,
- raw_json,
- })
- .map_err(|error| RadrootsSignerError::SigningFailed {
- message: error.to_string(),
- })
- }
-}
-
fn actor() -> RadrootsActorContext {
- RadrootsActorContext::test(SELLER, [RadrootsActorRole::Seller]).expect("actor")
+ RadrootsActorContext::test(seller_pubkey(), [RadrootsActorRole::Seller]).expect("actor")
}
-fn listing(d_tag: &str, title: &str) -> RadrootsListing {
- RadrootsListing {
+fn listing(d_tag: &str, title: &str) -> RadrootsOperationalListing {
+ RadrootsOperationalListing {
d_tag: RadrootsDTag::parse(d_tag).expect("d tag"),
published_at: None,
farm: RadrootsFarmRef {
- pubkey: SELLER.to_owned(),
+ pubkey: seller_pubkey().to_owned(),
d_tag: FARM_D_TAG.to_owned(),
},
- product: RadrootsListingProduct {
+ product: RadrootsOperationalListingProduct {
key: "coffee".to_owned(),
title: title.to_owned(),
category: "coffee".to_owned(),
@@ -455,7 +411,7 @@ fn listing(d_tag: &str, title: &str) -> RadrootsListing {
year: None,
},
primary_bin_id: RadrootsInventoryBinId::parse("bin-1").expect("bin id"),
- bins: vec![RadrootsListingBin {
+ bins: vec![RadrootsOperationalListingBin {
bin_id: RadrootsInventoryBinId::parse("bin-1").expect("bin id"),
quantity: RadrootsCoreQuantity::new(
RadrootsCoreDecimal::from(1000u32),
@@ -480,10 +436,18 @@ fn listing(d_tag: &str, title: &str) -> RadrootsListing {
resource_area: None,
plot: None,
discounts: None,
- inventory_available: None,
- availability: None,
- delivery_method: None,
- location: None,
+ inventory_available: Some(RadrootsCoreDecimal::from(5u32)),
+ availability: Some(RadrootsOperationalListingAvailability::Status {
+ status: RadrootsOperationalListingStatus::Active,
+ }),
+ delivery_method: Some(RadrootsOperationalListingDeliveryMethod::Pickup),
+ location: Some(RadrootsOperationalListingPublicLocation {
+ primary: "Victoria".to_owned(),
+ city: Some("Victoria".to_owned()),
+ region: Some("British Columbia".to_owned()),
+ country: Some("CA".to_owned()),
+ geohash: "c287g".to_owned(),
+ }),
images: None,
}
}
@@ -559,9 +523,9 @@ async fn enqueue_scoped_duplicate_listing(sdk: &RadrootsClient, d_tag: &str, tit
listing(d_tag, title),
))
.expect("prepared listing");
- let signer = FixtureSigner::new(SELLER);
+ let signer = FixtureSigner::new(seller_pubkey());
let signed_event = signer
- .sign_frozen_draft(&plan.frozen_draft)
+ .sign_frozen_draft(plan.frozen_draft())
.expect("signed listing");
let outbox = RadrootsOutbox::open_file(&sdk.storage_paths().expect("paths").runtime_path)
.await
@@ -569,7 +533,7 @@ async fn enqueue_scoped_duplicate_listing(sdk: &RadrootsClient, d_tag: &str, tit
outbox
.enqueue_signed_operation(RadrootsOutboxSignedOperationInput::new(
LISTING_PUBLISH_OPERATION_KIND,
- plan.frozen_draft,
+ plan.frozen_draft().clone(),
signed_event,
scoped_duplicate_relay_delivery_plan(RELAY_A),
true,
@@ -706,7 +670,7 @@ async fn enqueue_listing_with_policy(
.expect("relay targets")
.try_with_idempotency_key(sync_fixture_idempotency_key(d_tag, title, relays))
.expect("idempotency key"),
- &FixtureSigner::new(SELLER),
+ &FixtureSigner::new(seller_pubkey()),
)
.await
.expect("enqueue")
@@ -726,7 +690,7 @@ async fn sync_status_empty_store_reports_canonical_sources_and_transport_targets
assert_eq!(receipt.source, SyncStatusSource::SdkCanonicalStores);
assert_eq!(receipt.observed_at_ms, 1_700_000_000_000);
assert_eq!(receipt.event_store.total_events, 0);
- assert_eq!(receipt.event_store.projection_eligible_events, 0);
+ assert_eq!(receipt.event_store.valid_stream_events, 0);
assert_eq!(receipt.event_store.transport_observations, 0);
assert_eq!(receipt.event_store.last_event_seq, None);
assert_eq!(receipt.outbox.total_events, 0);
@@ -765,7 +729,7 @@ async fn sync_status_empty_store_reports_canonical_sources_and_transport_targets
"observed_at_ms": 1700000000000i64,
"event_store": {
"total_events": 0,
- "projection_eligible_events": 0,
+ "valid_stream_events": 0,
"transport_observations": 0,
"last_event_seq": null,
"last_event_updated_at_ms": null
@@ -1765,7 +1729,7 @@ async fn product_push_outbox_uses_radrootsd_execution_transport_with_daemon_reso
)
.try_with_idempotency_key("01890f0e-6c00-7000-8000-000000000250")
.expect("idempotency key"),
- &FixtureSigner::new(SELLER),
+ &FixtureSigner::new(seller_pubkey()),
)
.await
.expect("enqueue");
@@ -1873,7 +1837,7 @@ async fn product_push_outbox_radrootsd_execution_recovers_expired_publishing_cla
)
.try_with_idempotency_key("01890f0e-6c00-7000-8000-000000000251")
.expect("idempotency key"),
- &FixtureSigner::new(SELLER),
+ &FixtureSigner::new(seller_pubkey()),
)
.await
.expect("enqueue");
@@ -1978,7 +1942,7 @@ async fn product_push_outbox_radrootsd_execution_idempotency_is_attempt_scoped()
)
.try_with_idempotency_key("01890f0e-6c00-7000-8000-000000000252")
.expect("idempotency key"),
- &FixtureSigner::new(SELLER),
+ &FixtureSigner::new(seller_pubkey()),
)
.await
.expect("enqueue");
@@ -2087,7 +2051,7 @@ async fn product_push_outbox_radrootsd_execution_error_and_terminal_paths_update
)
.try_with_idempotency_key("01890f0e-6c00-7000-8000-000000000253")
.expect("idempotency key"),
- &FixtureSigner::new(SELLER),
+ &FixtureSigner::new(seller_pubkey()),
)
.await
.expect("enqueue retryable");
@@ -2152,7 +2116,7 @@ async fn product_push_outbox_radrootsd_execution_error_and_terminal_paths_update
)
.try_with_idempotency_key("01890f0e-6c00-7000-8000-000000000254")
.expect("idempotency key"),
- &FixtureSigner::new(SELLER),
+ &FixtureSigner::new(seller_pubkey()),
)
.await
.expect("enqueue terminal");
@@ -2335,7 +2299,7 @@ async fn sync_runtime_product_push_outbox_reticulum_reports_zero_attempts_with_r
)
.try_with_idempotency_key("01890f0e-6c00-7000-8000-000000000255")
.expect("idempotency key"),
- &FixtureSigner::new(SELLER),
+ &FixtureSigner::new(seller_pubkey()),
)
.await
.expect("enqueue");
@@ -2769,7 +2733,7 @@ async fn push_outbox_with_transport_scopes_duplicate_endpoint_sibling_plans() {
.expect("first targets")
.try_with_idempotency_key("01890f0e-6c00-7000-8000-00000000022e")
.expect("first idempotency"),
- &FixtureSigner::new(SELLER),
+ &FixtureSigner::new(seller_pubkey()),
)
.await
.expect("first enqueue");
@@ -3309,7 +3273,7 @@ async fn push_outbox_does_not_claim_unsigned_outbox_work() {
let unsigned = outbox
.enqueue_operation(RadrootsOutboxOperationInput::new(
LISTING_PUBLISH_OPERATION_KIND,
- prepared.frozen_draft,
+ prepared.frozen_draft().clone(),
delivery_plan_for_relays([RELAY_A], NostrRelayUrlPolicy::Public),
1_700_000_000_000,
))
diff --git a/crates/sdk/tests/unit/error_tests.rs b/crates/sdk/tests/unit/error_tests.rs
@@ -1,6 +1,6 @@
use super::{
- RadrootsSdkError, RadrootsSdkGeoNamesErrorKind, RadrootsSdkTradeErrorKind,
- radroots_sdk_error_catalog, redacted_relay_url,
+ RadrootsSdkError, RadrootsSdkGeoNamesErrorKind, RadrootsSdkListingValidationErrorKind,
+ RadrootsSdkTradeErrorKind, radroots_sdk_error_catalog, redacted_relay_url,
};
use crate::privacy::{PrivacyPreflightStatus, ProductSensitivityField};
use crate::transport::ReticulumBehavior;
@@ -33,7 +33,7 @@ fn authority_error_conversion_redacts_pubkey_mismatches_and_falls_back() {
#[test]
fn listing_and_store_errors_convert_to_sdk_error_classes() {
let draft = RadrootsSdkError::from(
- radroots_trade::listing::RadrootsListingEditError::ActorRoleUnsatisfied {
+ radroots_trade::operational_listing::RadrootsOperationalListingEditError::ActorRoleUnsatisfied {
required_role: RadrootsActorRole::Seller,
},
);
@@ -44,7 +44,7 @@ fn listing_and_store_errors_convert_to_sdk_error_classes() {
));
let draft_fallback = RadrootsSdkError::from(
- radroots_trade::listing::RadrootsListingEditError::InvalidFarmPubkey(
+ radroots_trade::operational_listing::RadrootsOperationalListingEditError::InvalidFarmPubkey(
radroots_event::ids::RadrootsIdParseError::InvalidCharacter,
),
);
@@ -54,8 +54,28 @@ fn listing_and_store_errors_convert_to_sdk_error_classes() {
if message.contains("invalid listing edit farm pubkey")
));
+ let invalid_model = RadrootsSdkError::from(
+ radroots_trade::operational_listing::RadrootsOperationalListingEditError::InvalidModel(
+ radroots_event::trade_validation::RadrootsOperationalListingValidationError::MissingInventory,
+ ),
+ );
+ assert!(matches!(
+ invalid_model,
+ RadrootsSdkError::ListingValidation {
+ kind: RadrootsSdkListingValidationErrorKind::MissingInventory,
+ ref message,
+ } if message == "missing listing inventory"
+ ));
+ let detail = invalid_model.detail_json();
+ assert_eq!(detail["code"], "listing_validation");
+ assert_eq!(
+ detail["detail"]["kind"],
+ serde_json::json!("missing_inventory")
+ );
+ assert_eq!(detail["detail"]["message"], "missing listing inventory");
+
let mutation = RadrootsSdkError::from(
- radroots_trade::listing::RadrootsListingMutationError::UnsupportedMutation,
+ radroots_trade::operational_listing::RadrootsOperationalListingMutationError::UnsupportedMutation,
);
assert!(matches!(
mutation,
@@ -343,6 +363,10 @@ fn sdk_error_contract_methods_cover_representative_classes_and_details() {
RadrootsSdkError::ListingEdit {
message: "edit".to_owned(),
},
+ RadrootsSdkError::ListingValidation {
+ kind: RadrootsSdkListingValidationErrorKind::MissingInventory,
+ message: "missing listing inventory".to_owned(),
+ },
RadrootsSdkError::ListingMutation {
message: "mutation".to_owned(),
},
@@ -414,6 +438,7 @@ fn sdk_error_catalog_exposes_stable_codes_and_metadata() {
assert!(codes.contains("product_sync_unsupported"));
assert!(codes.contains("reticulum_transport_deferred"));
assert!(codes.contains("unsupported_profile_schema"));
+ assert!(codes.contains("listing_validation"));
assert!(codes.contains("geonames_lookup"));
for trade_kind in [
diff --git a/crates/sdk/tests/unit/farms_runtime_tests.rs b/crates/sdk/tests/unit/farms_runtime_tests.rs
@@ -2,18 +2,21 @@ use super::*;
use crate::{RadrootsSdkLocalKeySigner, RadrootsSdkSignerProvider};
use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
-use crate::fixture_signer::FixtureSigner;
+use crate::fixture_signer::{FixtureSigner, fixture_alice_pubkey, fixture_bob_pubkey};
use crate::serializer_failure::assert_struct_serialize_error_paths;
-const FARMER: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
const FARM_A_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAAAA";
const FARM_B_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAAAQ";
const FARM_C_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAAAg";
const RELAY_A: &str = "wss://relay-a.radroots.test";
const RELAY_B: &str = "wss://relay-b.radroots.test";
+fn farmer_pubkey() -> &'static str {
+ fixture_alice_pubkey()
+}
+
fn farmer_actor() -> RadrootsActorContext {
- RadrootsActorContext::test(FARMER, [RadrootsActorRole::Farmer]).expect("actor")
+ RadrootsActorContext::test(farmer_pubkey(), [RadrootsActorRole::Farmer]).expect("actor")
}
fn farm(d_tag: &str, name: &str) -> RadrootsFarm {
@@ -154,7 +157,7 @@ fn farm_runtime_request_builders_and_serializers_cover_success_paths() {
.with_created_at(created_at);
assert_struct_serialize_error_paths(&prepare, 3);
let prepare_json = serde_json::to_value(&prepare).expect("prepare json");
- assert_eq!(prepare_json["actor"]["pubkey"], FARMER);
+ assert_eq!(prepare_json["actor"]["pubkey"], farmer_pubkey());
assert_eq!(prepare_json["created_at"], 1_700_000_321);
let enqueue = FarmEnqueuePublishRequest::new(
@@ -275,7 +278,7 @@ fn farm_runtime_request_builders_and_serializers_cover_success_paths() {
let private_receipt = FarmPrivateLocationReceipt {
farm_addr: farm_addr(&farmer_actor(), FARM_B_D_TAG).expect("farm addr"),
- farm_pubkey: FARMER.to_owned(),
+ farm_pubkey: farmer_pubkey().to_owned(),
farm_d_tag: FARM_B_D_TAG.to_owned(),
label: Some("query gate".to_owned()),
exact_location: SdkExactLocation::new(12.25, -34.5),
@@ -320,7 +323,7 @@ fn farm_runtime_request_builders_and_serializers_cover_success_paths() {
assert_struct_serialize_error_paths(&candidate, 7);
let lookup = FarmPrivateLocationLookupReceipt {
farm_addr: farm_addr(&farmer_actor(), FARM_B_D_TAG).expect("farm addr"),
- farm_pubkey: FARMER.to_owned(),
+ farm_pubkey: farmer_pubkey().to_owned(),
farm_d_tag: FARM_B_D_TAG.to_owned(),
input: FarmPrivateLocationInput::query("Fixture City, FX"),
candidates: vec![candidate],
@@ -479,7 +482,7 @@ async fn farm_client_prepare_resolves_default_and_explicit_created_at() {
))
.expect("default plan");
assert_eq!(
- default_plan.created_at,
+ default_plan.created_at(),
RadrootsSdkTimestamp::from_unix_seconds(1_700_000_400)
);
@@ -494,12 +497,162 @@ async fn farm_client_prepare_resolves_default_and_explicit_created_at() {
)
.expect("explicit plan");
assert_eq!(
- explicit_plan.created_at,
+ explicit_plan.created_at(),
RadrootsSdkTimestamp::from_unix_seconds(1_700_000_401)
);
}
#[tokio::test]
+async fn farm_prepared_plan_rejects_forged_state_before_signing_or_mutation() {
+ let sdk = crate::RadrootsClient::builder()
+ .fixed_clock(RadrootsSdkTimestamp::from_unix_seconds(1_700_000_450))
+ .build()
+ .await
+ .expect("sdk");
+ let actor = farmer_actor();
+ let plan = sdk
+ .farms()
+ .prepare_publish(FarmPreparePublishRequest::new(
+ actor.clone(),
+ farm(FARM_A_D_TAG, "Sealed Plan Farm"),
+ ))
+ .expect("plan");
+ assert_eq!(
+ serde_json::to_value(&plan).expect("plan json"),
+ serde_json::json!({
+ "farm_addr": plan.farm_addr(),
+ "expected_event_id": plan.expected_event_id(),
+ "frozen_draft": plan.frozen_draft(),
+ "created_at": plan.created_at(),
+ })
+ );
+ // Frozen drafts enforce registry contract-kind consistency, so a valid foreign pair is the
+ // safe representable substitution for both identity fields.
+ let foreign_draft = RadrootsEventDraft::new(
+ "radroots.social.geochat.v1",
+ radroots_event::kinds::KIND_GEOCHAT,
+ plan.created_at().unix_seconds(),
+ Vec::new(),
+ "Foreign draft",
+ farmer_pubkey(),
+ )
+ .expect("valid foreign draft");
+ let mut forged_contract_kind = plan.clone();
+ forged_contract_kind.frozen_draft = foreign_draft;
+ assert!(matches!(
+ validate_farm_publish_plan(&forged_contract_kind),
+ Err(RadrootsSdkError::InvalidRequest { ref message })
+ if message.contains("contract or kind")
+ ));
+
+ let mut forged_event_id = plan.clone();
+ forged_event_id.expected_event_id =
+ RadrootsEventId::parse(fixture_bob_pubkey()).expect("alternate event ID");
+ assert!(matches!(
+ validate_farm_publish_plan(&forged_event_id),
+ Err(RadrootsSdkError::InvalidRequest { ref message })
+ if message.contains("expected event ID")
+ ));
+
+ let mut forged_address = plan.clone();
+ forged_address.farm_addr = RadrootsAddressableCoordinate::parse(format!(
+ "{KIND_FARM}:{}:{FARM_B_D_TAG}",
+ farmer_pubkey()
+ ))
+ .expect("alternate farm address");
+ assert!(matches!(
+ validate_farm_publish_plan(&forged_address),
+ Err(RadrootsSdkError::InvalidRequest { ref message })
+ if message.contains("farm address")
+ ));
+
+ let mut forged_created_at = plan.clone();
+ forged_created_at.created_at = RadrootsSdkTimestamp::from_unix_seconds(1_700_000_451);
+ assert!(matches!(
+ validate_farm_publish_plan(&forged_created_at),
+ Err(RadrootsSdkError::InvalidRequest { ref message })
+ if message.contains("created-at timestamp")
+ ));
+
+ let alternate_plan = sdk
+ .farms()
+ .prepare_publish(FarmPreparePublishRequest::new(
+ actor.clone(),
+ farm(FARM_B_D_TAG, "Alternate Plan Farm"),
+ ))
+ .expect("alternate plan");
+ let mut forged_draft = plan;
+ forged_draft.frozen_draft = alternate_plan.frozen_draft;
+ assert!(matches!(
+ validate_farm_publish_plan(&forged_draft),
+ Err(RadrootsSdkError::InvalidRequest { ref message })
+ if message.contains("expected event ID")
+ ));
+
+ let error = sdk
+ .farms()
+ .enqueue_prepared_publish_with_explicit_signer(
+ &actor,
+ forged_contract_kind,
+ TargetPolicy::try_nostr_relays([RELAY_A], NostrRelayUrlPolicy::Public)
+ .expect("transport targets"),
+ Some(
+ SdkIdempotencyKey::new("01890f0e-6c00-7000-8000-000000000445")
+ .expect("idempotency"),
+ ),
+ &FixtureSigner::new(fixture_bob_pubkey()),
+ )
+ .await
+ .expect_err("forged plan");
+ assert!(matches!(
+ error,
+ RadrootsSdkError::InvalidRequest { ref message }
+ if message.contains("contract or kind")
+ ));
+ let error = sdk
+ .farms()
+ .enqueue_prepared_publish(
+ &actor,
+ forged_address,
+ TargetPolicy::try_nostr_relays([RELAY_A], NostrRelayUrlPolicy::Public)
+ .expect("transport targets"),
+ Some(
+ SdkIdempotencyKey::new("01890f0e-6c00-7000-8000-000000000446")
+ .expect("idempotency"),
+ ),
+ )
+ .await
+ .expect_err("forged configured-signer plan");
+ assert!(matches!(
+ error,
+ RadrootsSdkError::InvalidRequest { ref message }
+ if message.contains("farm address")
+ ));
+ assert_eq!(
+ sdk._event_store
+ .status_summary()
+ .await
+ .expect("event store status")
+ .total_events,
+ 0
+ );
+ assert_eq!(
+ sdk._outbox
+ .status_summary(0)
+ .await
+ .expect("outbox status")
+ .total_events,
+ 0
+ );
+ let journal_count: i64 =
+ sqlx::query_scalar("SELECT COUNT(*) FROM sdk_runtime_operation_journal")
+ .fetch_one(sdk._event_store.pool())
+ .await
+ .expect("journal count");
+ assert_eq!(journal_count, 0);
+}
+
+#[tokio::test]
async fn farm_client_prepare_reports_clock_errors() {
let sdk = crate::RadrootsClient::builder()
.clock(crate::RadrootsSdkClock::BeforeUnixEpoch)
@@ -532,7 +685,7 @@ async fn farm_enqueue_publish_reports_prepare_errors_before_signing() {
TargetPolicy::try_nostr_relays([RELAY_A], NostrRelayUrlPolicy::Public)
.expect("transport targets"),
),
- &FixtureSigner::new(FARMER),
+ &FixtureSigner::new(farmer_pubkey()),
)
.await
.expect_err("prepare error");
@@ -546,7 +699,7 @@ async fn farm_client_enqueue_methods_cover_source_attached_workflow_paths() {
.build()
.await
.expect("sdk");
- let signer = FixtureSigner::new(FARMER);
+ let signer = FixtureSigner::new(farmer_pubkey());
let actor = farmer_actor();
let receipt = sdk
.farms()
@@ -597,13 +750,14 @@ async fn farm_configured_local_signer_enqueues_publish_without_explicit_signer()
let sdk = crate::RadrootsClient::builder()
.fixed_clock(RadrootsSdkTimestamp::from_unix_seconds(1_700_000_500))
.signer_provider(RadrootsSdkSignerProvider::LocalKey(
- RadrootsSdkLocalKeySigner::from_event_signer(FixtureSigner::new(FARMER))
+ RadrootsSdkLocalKeySigner::from_event_signer(FixtureSigner::new(farmer_pubkey()))
.expect("signer"),
))
.build()
.await
.expect("sdk");
- let actor = RadrootsActorContext::test(FARMER, [RadrootsActorRole::Farmer]).expect("actor");
+ let actor =
+ RadrootsActorContext::test(farmer_pubkey(), [RadrootsActorRole::Farmer]).expect("actor");
let receipt = sdk
.farms()
@@ -629,13 +783,14 @@ async fn farm_configured_enqueue_reports_prepare_and_signer_errors() {
let configured_sdk = crate::RadrootsClient::builder()
.fixed_clock(RadrootsSdkTimestamp::from_unix_seconds(1_700_000_500))
.signer_provider(RadrootsSdkSignerProvider::LocalKey(
- RadrootsSdkLocalKeySigner::from_event_signer(FixtureSigner::new(FARMER))
+ RadrootsSdkLocalKeySigner::from_event_signer(FixtureSigner::new(farmer_pubkey()))
.expect("signer"),
))
.build()
.await
.expect("configured sdk");
- let actor = RadrootsActorContext::test(FARMER, [RadrootsActorRole::Farmer]).expect("actor");
+ let actor =
+ RadrootsActorContext::test(farmer_pubkey(), [RadrootsActorRole::Farmer]).expect("actor");
assert!(matches!(
configured_sdk
@@ -767,8 +922,8 @@ async fn farm_private_location_default_client_and_lookup_report_store_edges() {
farm_addr(&actor, FARM_B_D_TAG).expect("farm b addr")
);
- let non_farmer_actor =
- RadrootsActorContext::test(FARMER, [RadrootsActorRole::Buyer]).expect("buyer actor");
+ let non_farmer_actor = RadrootsActorContext::test(farmer_pubkey(), [RadrootsActorRole::Buyer])
+ .expect("buyer actor");
assert!(matches!(
sdk.farms()
.clear_private_location(FarmPrivateLocationClearRequest::new(
diff --git a/crates/sdk/tests/unit/listings_runtime_tests.rs b/crates/sdk/tests/unit/listings_runtime_tests.rs
@@ -8,14 +8,17 @@ use radroots_event::{
contract::RadrootsActorRole,
farm::RadrootsFarmRef,
ids::{RadrootsDTag, RadrootsInventoryBinId},
- listing::{RadrootsListingBin, RadrootsListingProduct},
+ operational_listing::{
+ RadrootsOperationalListingAvailability, RadrootsOperationalListingBin,
+ RadrootsOperationalListingDeliveryMethod, RadrootsOperationalListingProduct,
+ RadrootsOperationalListingPublicLocation, RadrootsOperationalListingStatus,
+ },
resource_area::RadrootsResourceAreaRef,
};
-use crate::fixture_signer::FixtureSigner;
+use crate::fixture_signer::{FixtureSigner, fixture_alice_pubkey, fixture_bob_pubkey};
use crate::serializer_failure::assert_struct_serialize_error_paths;
-const SELLER: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
const FARM_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAAAA";
const LISTING_A_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAAAQ";
const LISTING_B_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAAAg";
@@ -23,23 +26,27 @@ const LISTING_C_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAAAw";
const RELAY_A: &str = "wss://relay-a.radroots.test";
const RELAY_B: &str = "wss://relay-b.radroots.test";
+fn seller_pubkey() -> &'static str {
+ fixture_alice_pubkey()
+}
+
fn actor() -> RadrootsActorContext {
- RadrootsActorContext::test(SELLER, [RadrootsActorRole::Seller]).expect("actor")
+ RadrootsActorContext::test(seller_pubkey(), [RadrootsActorRole::Seller]).expect("actor")
}
-fn listing(d_tag: &str, title: &str) -> RadrootsListing {
- listing_for_seller(SELLER, d_tag, title)
+fn listing(d_tag: &str, title: &str) -> RadrootsOperationalListing {
+ listing_for_seller(seller_pubkey(), d_tag, title)
}
-fn listing_for_seller(seller: &str, d_tag: &str, title: &str) -> RadrootsListing {
- RadrootsListing {
+fn listing_for_seller(seller: &str, d_tag: &str, title: &str) -> RadrootsOperationalListing {
+ RadrootsOperationalListing {
d_tag: RadrootsDTag::parse(d_tag).expect("d tag"),
published_at: None,
farm: RadrootsFarmRef {
pubkey: seller.to_owned(),
d_tag: FARM_D_TAG.to_owned(),
},
- product: RadrootsListingProduct {
+ product: RadrootsOperationalListingProduct {
key: "lettuce".to_owned(),
title: title.to_owned(),
category: "greens".to_owned(),
@@ -51,7 +58,7 @@ fn listing_for_seller(seller: &str, d_tag: &str, title: &str) -> RadrootsListing
year: None,
},
primary_bin_id: RadrootsInventoryBinId::parse("bin-1").expect("bin id"),
- bins: vec![RadrootsListingBin {
+ bins: vec![RadrootsOperationalListingBin {
bin_id: RadrootsInventoryBinId::parse("bin-1").expect("bin id"),
quantity: RadrootsCoreQuantity::new(
RadrootsCoreDecimal::from(12u32),
@@ -76,10 +83,18 @@ fn listing_for_seller(seller: &str, d_tag: &str, title: &str) -> RadrootsListing
resource_area: None,
plot: None,
discounts: None,
- inventory_available: None,
- availability: None,
- delivery_method: None,
- location: None,
+ inventory_available: Some(RadrootsCoreDecimal::from(12u32)),
+ availability: Some(RadrootsOperationalListingAvailability::Status {
+ status: RadrootsOperationalListingStatus::Active,
+ }),
+ delivery_method: Some(RadrootsOperationalListingDeliveryMethod::Pickup),
+ location: Some(RadrootsOperationalListingPublicLocation {
+ primary: "Victoria".to_owned(),
+ city: Some("Victoria".to_owned()),
+ region: Some("British Columbia".to_owned()),
+ country: Some("CA".to_owned()),
+ geohash: "c287g".to_owned(),
+ }),
images: None,
}
}
@@ -92,12 +107,12 @@ fn listing_runtime_request_builders_and_serializers_cover_success_paths() {
.with_created_at(created_at);
assert_struct_serialize_error_paths(&prepare, 3);
let prepare_json = serde_json::to_value(&prepare).expect("prepare json");
- assert_eq!(prepare_json["actor"]["pubkey"], SELLER);
+ assert_eq!(prepare_json["actor"]["pubkey"], seller_pubkey());
assert_eq!(prepare_json["created_at"], 1_700_000_321);
let enqueue = ListingEnqueuePublishRequest::from_document(
actor(),
- RadrootsListingEditDocumentV1::new(listing(LISTING_B_D_TAG, "Queued Greens")),
+ RadrootsOperationalListingEditDocumentV1::new(listing(LISTING_B_D_TAG, "Queued Greens")),
TargetPolicy::default_profile(),
)
.try_with_nostr_targets([RELAY_A, RELAY_B], NostrRelayUrlPolicy::Public)
@@ -141,7 +156,7 @@ fn listing_request_builders_reject_invalid_options_and_timestamp_bounds() {
let timestamp_error = listing_publish_plan(
&actor(),
- RadrootsListingEditDocumentV1::new(listing(LISTING_B_D_TAG, "Future Greens")),
+ RadrootsOperationalListingEditDocumentV1::new(listing(LISTING_B_D_TAG, "Future Greens")),
RadrootsSdkTimestamp::from_unix_seconds(u64::MAX),
)
.expect_err("timestamp error");
@@ -153,12 +168,12 @@ fn listing_request_builders_reject_invalid_options_and_timestamp_bounds() {
let mut invalid_resource_area_listing =
listing(LISTING_C_D_TAG, "Invalid Resource Area Greens");
invalid_resource_area_listing.resource_area = Some(RadrootsResourceAreaRef {
- pubkey: SELLER.to_owned(),
+ pubkey: seller_pubkey().to_owned(),
d_tag: "bad d tag".to_owned(),
});
let mutation_error = listing_publish_plan(
&actor(),
- RadrootsListingEditDocumentV1::new(invalid_resource_area_listing),
+ RadrootsOperationalListingEditDocumentV1::new(invalid_resource_area_listing),
RadrootsSdkTimestamp::from_unix_seconds(1_700_000_000),
)
.expect_err("mutation error");
@@ -183,7 +198,7 @@ async fn listing_client_prepare_resolves_default_and_explicit_created_at() {
))
.expect("default plan");
assert_eq!(
- default_plan.created_at,
+ default_plan.created_at(),
RadrootsSdkTimestamp::from_unix_seconds(1_700_000_400)
);
@@ -198,12 +213,162 @@ async fn listing_client_prepare_resolves_default_and_explicit_created_at() {
)
.expect("explicit plan");
assert_eq!(
- explicit_plan.created_at,
+ explicit_plan.created_at(),
RadrootsSdkTimestamp::from_unix_seconds(1_700_000_401)
);
}
#[tokio::test]
+async fn listing_prepared_plan_rejects_forged_state_before_signing_or_mutation() {
+ let sdk = crate::RadrootsClient::builder()
+ .fixed_clock(RadrootsSdkTimestamp::from_unix_seconds(1_700_000_450))
+ .build()
+ .await
+ .expect("sdk");
+ let actor = actor();
+ let plan = sdk
+ .listings()
+ .prepare_publish(ListingPreparePublishRequest::new(
+ actor.clone(),
+ listing(LISTING_A_D_TAG, "Sealed Plan Greens"),
+ ))
+ .expect("plan");
+ assert_eq!(
+ serde_json::to_value(&plan).expect("plan json"),
+ serde_json::json!({
+ "public_listing_addr": plan.public_listing_addr(),
+ "expected_event_id": plan.expected_event_id(),
+ "frozen_draft": plan.frozen_draft(),
+ "created_at": plan.created_at(),
+ })
+ );
+ // Frozen drafts enforce registry contract-kind consistency, so a valid foreign pair is the
+ // safe representable substitution for both identity fields.
+ let foreign_draft = RadrootsEventDraft::new(
+ "radroots.social.geochat.v1",
+ radroots_event::kinds::KIND_GEOCHAT,
+ plan.created_at().unix_seconds(),
+ Vec::new(),
+ "Foreign draft",
+ seller_pubkey(),
+ )
+ .expect("valid foreign draft");
+ let mut forged_contract_kind = plan.clone();
+ forged_contract_kind.frozen_draft = foreign_draft;
+ assert!(matches!(
+ validate_listing_publish_plan(&forged_contract_kind),
+ Err(RadrootsSdkError::InvalidRequest { ref message })
+ if message.contains("contract or kind")
+ ));
+
+ let mut forged_event_id = plan.clone();
+ forged_event_id.expected_event_id =
+ RadrootsEventId::parse(fixture_bob_pubkey()).expect("alternate event ID");
+ assert!(matches!(
+ validate_listing_publish_plan(&forged_event_id),
+ Err(RadrootsSdkError::InvalidRequest { ref message })
+ if message.contains("expected event ID")
+ ));
+
+ let mut forged_address = plan.clone();
+ forged_address.public_listing_addr = RadrootsClassifiedListingAddress::parse(format!(
+ "{KIND_CLASSIFIED_LISTING}:{}:{LISTING_B_D_TAG}",
+ seller_pubkey()
+ ))
+ .expect("alternate listing address");
+ assert!(matches!(
+ validate_listing_publish_plan(&forged_address),
+ Err(RadrootsSdkError::InvalidRequest { ref message })
+ if message.contains("listing address")
+ ));
+
+ let mut forged_created_at = plan.clone();
+ forged_created_at.created_at = RadrootsSdkTimestamp::from_unix_seconds(1_700_000_451);
+ assert!(matches!(
+ validate_listing_publish_plan(&forged_created_at),
+ Err(RadrootsSdkError::InvalidRequest { ref message })
+ if message.contains("created-at timestamp")
+ ));
+
+ let alternate_plan = sdk
+ .listings()
+ .prepare_publish(ListingPreparePublishRequest::new(
+ actor.clone(),
+ listing(LISTING_B_D_TAG, "Alternate Plan Greens"),
+ ))
+ .expect("alternate plan");
+ let mut forged_draft = plan;
+ forged_draft.frozen_draft = alternate_plan.frozen_draft;
+ assert!(matches!(
+ validate_listing_publish_plan(&forged_draft),
+ Err(RadrootsSdkError::InvalidRequest { ref message })
+ if message.contains("expected event ID")
+ ));
+
+ let error = sdk
+ .listings()
+ .enqueue_prepared_publish_with_explicit_signer(
+ &actor,
+ forged_contract_kind,
+ TargetPolicy::try_nostr_relays([RELAY_A], NostrRelayUrlPolicy::Public)
+ .expect("transport targets"),
+ Some(
+ SdkIdempotencyKey::new("01890f0e-6c00-7000-8000-000000000245")
+ .expect("idempotency"),
+ ),
+ &FixtureSigner::new(fixture_bob_pubkey()),
+ )
+ .await
+ .expect_err("forged plan");
+ assert!(matches!(
+ error,
+ RadrootsSdkError::InvalidRequest { ref message }
+ if message.contains("contract or kind")
+ ));
+ let error = sdk
+ .listings()
+ .enqueue_prepared_publish(
+ &actor,
+ forged_address,
+ TargetPolicy::try_nostr_relays([RELAY_A], NostrRelayUrlPolicy::Public)
+ .expect("transport targets"),
+ Some(
+ SdkIdempotencyKey::new("01890f0e-6c00-7000-8000-000000000246")
+ .expect("idempotency"),
+ ),
+ )
+ .await
+ .expect_err("forged configured-signer plan");
+ assert!(matches!(
+ error,
+ RadrootsSdkError::InvalidRequest { ref message }
+ if message.contains("listing address")
+ ));
+ assert_eq!(
+ sdk._event_store
+ .status_summary()
+ .await
+ .expect("event store status")
+ .total_events,
+ 0
+ );
+ assert_eq!(
+ sdk._outbox
+ .status_summary(0)
+ .await
+ .expect("outbox status")
+ .total_events,
+ 0
+ );
+ let journal_count: i64 =
+ sqlx::query_scalar("SELECT COUNT(*) FROM sdk_runtime_operation_journal")
+ .fetch_one(sdk._event_store.pool())
+ .await
+ .expect("journal count");
+ assert_eq!(journal_count, 0);
+}
+
+#[tokio::test]
async fn listing_client_prepare_reports_clock_errors() {
let sdk = crate::RadrootsClient::builder()
.clock(crate::RadrootsSdkClock::BeforeUnixEpoch)
@@ -237,7 +402,7 @@ async fn listing_enqueue_publish_reports_prepare_errors_before_signing() {
.expect("transport targets"),
)
.with_created_at(RadrootsSdkTimestamp::from_unix_seconds(u64::MAX)),
- &FixtureSigner::new(SELLER),
+ &FixtureSigner::new(seller_pubkey()),
)
.await
.expect_err("prepare error");
@@ -245,6 +410,53 @@ async fn listing_enqueue_publish_reports_prepare_errors_before_signing() {
error,
RadrootsSdkError::TimestampOutOfRange { .. }
));
+
+ let mut invalid_listing = listing(LISTING_B_D_TAG, "Invalid Inventory Greens");
+ invalid_listing.inventory_available = None;
+ let error = sdk
+ .listings()
+ .enqueue_publish_with_explicit_signer(
+ ListingEnqueuePublishRequest::new(
+ actor(),
+ invalid_listing,
+ TargetPolicy::try_nostr_relays([RELAY_A], NostrRelayUrlPolicy::Public)
+ .expect("transport targets"),
+ )
+ .try_with_idempotency_key("01890f0e-6c00-7000-8000-000000000239")
+ .expect("idempotency"),
+ &FixtureSigner::new(fixture_bob_pubkey()),
+ )
+ .await
+ .expect_err("invalid listing model");
+ assert!(matches!(
+ error,
+ RadrootsSdkError::ListingValidation {
+ kind: crate::RadrootsSdkListingValidationErrorKind::MissingInventory,
+ ref message,
+ } if message == "missing listing inventory"
+ ));
+ assert_eq!(
+ sdk._event_store
+ .status_summary()
+ .await
+ .expect("event store status")
+ .total_events,
+ 0
+ );
+ assert_eq!(
+ sdk._outbox
+ .status_summary(0)
+ .await
+ .expect("outbox status")
+ .total_events,
+ 0
+ );
+ let journal_count: i64 =
+ sqlx::query_scalar("SELECT COUNT(*) FROM sdk_runtime_operation_journal")
+ .fetch_one(sdk._event_store.pool())
+ .await
+ .expect("journal count");
+ assert_eq!(journal_count, 0);
}
#[tokio::test]
@@ -254,7 +466,7 @@ async fn listing_client_enqueue_methods_cover_source_attached_workflow_paths() {
.build()
.await
.expect("sdk");
- let signer = FixtureSigner::new(SELLER);
+ let signer = FixtureSigner::new(seller_pubkey());
let actor = actor();
let receipt = sdk
.listings()
@@ -278,7 +490,10 @@ async fn listing_client_enqueue_methods_cover_source_attached_workflow_paths() {
.listings()
.prepare_publish(ListingPreparePublishRequest::from_document(
actor.clone(),
- RadrootsListingEditDocumentV1::new(listing(LISTING_B_D_TAG, "Prepared Greens")),
+ RadrootsOperationalListingEditDocumentV1::new(listing(
+ LISTING_B_D_TAG,
+ "Prepared Greens",
+ )),
))
.expect("prepared listing");
let prepared = sdk
@@ -305,20 +520,21 @@ async fn listing_configured_local_signer_enqueues_publish_without_explicit_signe
let sdk = crate::RadrootsClient::builder()
.fixed_clock(RadrootsSdkTimestamp::from_unix_seconds(1_700_000_500))
.signer_provider(RadrootsSdkSignerProvider::LocalKey(
- RadrootsSdkLocalKeySigner::from_event_signer(FixtureSigner::new(SELLER))
+ RadrootsSdkLocalKeySigner::from_event_signer(FixtureSigner::new(seller_pubkey()))
.expect("signer"),
))
.build()
.await
.expect("sdk");
- let actor = RadrootsActorContext::test(SELLER, [RadrootsActorRole::Seller]).expect("actor");
+ let actor =
+ RadrootsActorContext::test(seller_pubkey(), [RadrootsActorRole::Seller]).expect("actor");
let receipt = sdk
.listings()
.enqueue_publish(
ListingEnqueuePublishRequest::new(
actor,
- listing_for_seller(SELLER, LISTING_C_D_TAG, "Configured Greens"),
+ listing_for_seller(seller_pubkey(), LISTING_C_D_TAG, "Configured Greens"),
TargetPolicy::try_nostr_relays([RELAY_A], NostrRelayUrlPolicy::Public)
.expect("transport targets"),
)
diff --git a/crates/sdk/tests/unit/runtime_tests.rs b/crates/sdk/tests/unit/runtime_tests.rs
@@ -100,7 +100,7 @@ fn storage_status() -> StorageStatusReceipt {
event_store: SdkEventStoreStorageStatus {
store: sqlite_status(),
total_events: 0,
- projection_eligible_events: 0,
+ valid_stream_events: 0,
transport_observations: 0,
last_event_seq: None,
last_event_updated_at_ms: None,
@@ -774,6 +774,82 @@ fn sqlite_wal_checkpoint_receipt_mapping_covers_edge_states() {
}
#[tokio::test]
+async fn read_only_event_store_status_enforces_valid_stream_invariants() {
+ let sdk = RadrootsClient::builder().build().await.expect("sdk");
+ let pool = sdk._event_store.pool();
+ let valid_event_id = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
+ let unsupported_event_id = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
+
+ sqlx::query(
+ "INSERT INTO event_envelopes(event_id, pubkey, created_at, kind, tags_json, content, sig, raw_json, verification_status, contract_status, contract_id, event_class, projection_eligible, inserted_at_ms, updated_at_ms) VALUES (?, ?, 1, 1, '[]', 'valid', ?, '{}', 'verified', 'admitted', 'radroots.social.post.v1', 'regular', 1, 1, 1)",
+ )
+ .bind(valid_event_id)
+ .bind("c".repeat(64))
+ .bind("d".repeat(128))
+ .execute(pool)
+ .await
+ .expect("valid event row");
+ sqlx::query(
+ "INSERT INTO event_envelopes(event_id, pubkey, created_at, kind, tags_json, content, sig, raw_json, verification_status, contract_status, contract_id, event_class, projection_eligible, inserted_at_ms, updated_at_ms) VALUES (?, ?, 2, 65000, '[]', 'unsupported', ?, '{}', 'verified', 'unsupported', NULL, 'regular', 0, 2, 2)",
+ )
+ .bind(unsupported_event_id)
+ .bind("e".repeat(64))
+ .bind("f".repeat(128))
+ .execute(pool)
+ .await
+ .expect("unsupported event row");
+
+ let summary = event_store_status_summary_from_pool(pool)
+ .await
+ .expect("valid stream summary");
+ assert_eq!(summary.total_events, 2);
+ assert_eq!(summary.valid_stream_events, 1);
+
+ sqlx::query(
+ "UPDATE event_envelopes SET contract_status = 'supported', contract_id = 'radroots.legacy.supported.v0', projection_eligible = 1 WHERE event_id = ?",
+ )
+ .bind(unsupported_event_id)
+ .execute(pool)
+ .await
+ .expect("legacy status");
+ let legacy_summary = event_store_status_summary_from_pool(pool)
+ .await
+ .expect("legacy status summary");
+ assert_eq!(legacy_summary.total_events, 2);
+ assert_eq!(legacy_summary.valid_stream_events, 1);
+ sqlx::query(
+ "UPDATE event_envelopes SET contract_status = 'unsupported', contract_id = NULL, projection_eligible = 0 WHERE event_id = ?",
+ )
+ .bind(unsupported_event_id)
+ .execute(pool)
+ .await
+ .expect("restore admission status");
+
+ sqlx::query(
+ "UPDATE event_envelopes SET verification_status = 'signature_invalid' WHERE event_id = ?",
+ )
+ .bind(valid_event_id)
+ .execute(pool)
+ .await
+ .expect("invalid verification");
+ assert_event_store_error(event_store_status_summary_from_pool(pool).await);
+ sqlx::query("UPDATE event_envelopes SET verification_status = 'verified' WHERE event_id = ?")
+ .bind(valid_event_id)
+ .execute(pool)
+ .await
+ .expect("restore verification");
+
+ sqlx::query(
+ "UPDATE event_envelopes SET kind = 20000, event_class = 'ephemeral', projection_eligible = 0 WHERE event_id = ?",
+ )
+ .bind(valid_event_id)
+ .execute(pool)
+ .await
+ .expect("ephemeral stored row");
+ assert_event_store_error(event_store_status_summary_from_pool(pool).await);
+}
+
+#[tokio::test]
async fn storage_status_inspection_is_read_only_and_never_creates_missing_profiles() {
let tempdir = tempfile::tempdir().expect("tempdir");
let missing = tempdir.path().join("missing-profile");
@@ -782,6 +858,28 @@ async fn storage_status_inspection_is_read_only_and_never_creates_missing_profil
);
assert!(!missing.exists());
+ let legacy_profile = tempdir.path().join("legacy-profile");
+ let legacy_sdk = RadrootsClient::builder()
+ .directory_storage(&legacy_profile)
+ .build()
+ .await
+ .expect("legacy sdk");
+ sqlx::query(
+ "INSERT INTO event_envelopes(event_id, pubkey, created_at, kind, tags_json, content, sig, raw_json, verification_status, contract_status, contract_id, event_class, projection_eligible, inserted_at_ms, updated_at_ms) VALUES (?, ?, 1, 1, '[]', 'legacy', ?, '{}', 'verified', 'supported', 'radroots.social.post.v1', NULL, 1, 1, 1)",
+ )
+ .bind("cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc")
+ .bind("d".repeat(64))
+ .bind("e".repeat(128))
+ .execute(legacy_sdk._event_store.pool())
+ .await
+ .expect("legacy event row");
+ let legacy_status =
+ RadrootsClient::inspect_storage_status(&legacy_profile, StorageStatusRequest::new())
+ .await
+ .expect("legacy storage status");
+ assert_eq!(legacy_status.event_store.total_events, 1);
+ assert_eq!(legacy_status.event_store.valid_stream_events, 0);
+
let pre_v1_profile = tempdir.path().join("pre-v1");
fs::create_dir(&pre_v1_profile).expect("pre-v1 profile");
fs::write(pre_v1_profile.join("event_store.sqlite"), b"old runtime").expect("old event store");
@@ -1876,40 +1974,29 @@ async fn restore_archive_private_failures_cover_staging_and_verification_edges()
.build()
.await
.expect("populated sdk");
- let populated_event_pubkey = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
- let populated_event_tags = Vec::<Vec<String>>::new();
- let populated_event_content = "{}".to_owned();
- let populated_event_id = radroots_event::wire::compute_canonical_nip01_event_id(
- populated_event_pubkey,
+ let populated_event_keys = radroots_nostr::prelude::RadrootsNostrKeys::generate();
+ let populated_event_draft = radroots_event::draft::RadrootsEventDraft::new(
+ "radroots.farm.profile.v1",
+ radroots_event::kinds::KIND_FARM,
1_700_000_002,
- 1,
- &populated_event_tags,
- &populated_event_content,
+ vec![vec!["d".to_owned(), "backup-fixture".to_owned()]],
+ "{}",
+ populated_event_keys.public_key().to_hex(),
)
- .expect("canonical event id");
- let populated_event_wire = radroots_event::wire::RadrootsNip01EventWire {
- id: populated_event_id.into_string(),
- pubkey: populated_event_pubkey.to_owned(),
- created_at: 1_700_000_002,
- kind: 1,
- tags: populated_event_tags,
- content: populated_event_content,
- sig: "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff".to_owned(),
- extra: Default::default(),
- };
- let populated_event_raw_json =
- serde_json::to_string(&populated_event_wire).expect("raw event json");
- let populated_event = radroots_event::draft::RadrootsSignedEvent::from_wire_verified_id(
- populated_event_wire,
- populated_event_raw_json,
+ .expect("event draft");
+ let populated_event = radroots_nostr::prelude::radroots_nostr_sign_frozen_draft(
+ &populated_event_keys,
+ &populated_event_draft,
)
.expect("signed event");
+ let populated_ingest = radroots_event_store::RadrootsEventIngest::from_signed_event(
+ populated_event,
+ 1_700_000_002_000,
+ )
+ .expect("verified event ingest");
populated_sdk
._event_store
- .ingest_event(radroots_event_store::RadrootsEventIngest::new(
- populated_event,
- 1_700_000_002_000,
- ))
+ .ingest_event(populated_ingest)
.await
.expect("populated event");
let populated_backup_destination = tempdir.path().join("populated-backup");
diff --git a/crates/sdk/tests/unit/signer_provider_tests.rs b/crates/sdk/tests/unit/signer_provider_tests.rs
@@ -4,34 +4,31 @@ use nostr::{EventBuilder, JsonUtil, Kind, Tag};
use radroots_authority::RadrootsLocalEventSigner;
use radroots_event::contract::RadrootsActorRole;
use radroots_event::draft::RadrootsEventDraft;
-use radroots_event::kinds::{KIND_COOP, KIND_FARM, KIND_LISTING, TRADE_MUTATION_EVENT_KINDS};
-use radroots_nostr::prelude::{RadrootsNostrEvent, RadrootsNostrSecretKey};
+use radroots_event::kinds::{
+ KIND_CLASSIFIED_LISTING, KIND_COOP, KIND_FARM, TRADE_MUTATION_EVENT_KINDS,
+};
+use radroots_nostr::prelude::RadrootsNostrEvent;
use radroots_nostr_connect::prelude::{
RADROOTS_NOSTR_CONNECT_RPC_KIND, RadrootsNostrConnectClientTarget, RadrootsNostrConnectError,
RadrootsNostrConnectRequest, RadrootsNostrConnectRequestMessage, RadrootsNostrConnectResponse,
};
use std::collections::VecDeque;
use std::future;
-use std::sync::{Arc, Mutex};
+use std::sync::{Arc, LazyLock, Mutex};
use std::time::Duration;
use uuid::Uuid;
-const USER_SECRET_KEY_HEX: &str =
- "10c5304d6c9ae3a1a16f7860f1cc8f5e3a76225a2663b3a989a0d775919b7df5";
-const USER_PUBLIC_KEY_HEX: &str =
- "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df";
-const REMOTE_SECRET_KEY_HEX: &str =
- "59392e9068f66431b12f70218fb61281cb6b433d7f27c55d61f1a63fe1a96ff8";
-const CLIENT_SECRET_KEY_HEX: &str =
- "4d6c20fdd86857de77ff5cfa5c545751ba2efd126e0b6642dae9764d782d6509";
-
-fn keys(secret_key_hex: &str) -> RadrootsNostrKeys {
- let secret_key = RadrootsNostrSecretKey::from_hex(secret_key_hex).expect("secret key");
- RadrootsNostrKeys::new(secret_key)
-}
+static USER_KEYS: LazyLock<RadrootsNostrKeys> = LazyLock::new(RadrootsNostrKeys::generate);
+static USER_PUBLIC_KEY: LazyLock<String> = LazyLock::new(|| USER_KEYS.public_key().to_hex());
+static REMOTE_KEYS: LazyLock<RadrootsNostrKeys> = LazyLock::new(RadrootsNostrKeys::generate);
+static CLIENT_KEYS: LazyLock<RadrootsNostrKeys> = LazyLock::new(RadrootsNostrKeys::generate);
fn user_keys() -> RadrootsNostrKeys {
- keys(USER_SECRET_KEY_HEX)
+ USER_KEYS.clone()
+}
+
+fn user_pubkey() -> &'static str {
+ USER_PUBLIC_KEY.as_str()
}
fn local_sdk_signer(keys: RadrootsNostrKeys) -> RadrootsSdkLocalKeySigner {
@@ -42,21 +39,21 @@ fn local_sdk_signer(keys: RadrootsNostrKeys) -> RadrootsSdkLocalKeySigner {
}
fn remote_keys() -> RadrootsNostrKeys {
- keys(REMOTE_SECRET_KEY_HEX)
+ REMOTE_KEYS.clone()
}
fn client_keys() -> RadrootsNostrKeys {
- keys(CLIENT_SECRET_KEY_HEX)
+ CLIENT_KEYS.clone()
}
fn actor() -> RadrootsActorContext {
- RadrootsActorContext::test(USER_PUBLIC_KEY_HEX, [RadrootsActorRole::Farmer]).expect("actor")
+ RadrootsActorContext::test(user_pubkey(), [RadrootsActorRole::Farmer]).expect("actor")
}
fn frozen_draft() -> RadrootsEventDraft {
frozen_draft_with(
"radroots.farm.profile.v1",
- USER_PUBLIC_KEY_HEX,
+ user_pubkey(),
KIND_FARM,
1_700_000_000,
vec![vec!["d".to_owned(), "sdk-signer".to_owned()]],
@@ -121,13 +118,9 @@ fn myc_signer_with_responses(
remote_keys.public_key(),
vec![nostr::RelayUrl::parse("wss://relay.example.com").expect("relay")],
);
- let signer = RadrootsSdkMycNip46Signer::new(
- client_keys(),
- target,
- USER_PUBLIC_KEY_HEX,
- transport.clone(),
- )
- .expect("signer");
+ let signer =
+ RadrootsSdkMycNip46Signer::new(client_keys(), target, user_pubkey(), transport.clone())
+ .expect("signer");
(signer, transport)
}
@@ -260,7 +253,7 @@ async fn local_key_provider_signs_authorized_frozen_draft() {
assert_eq!(provider.status(), signer.status());
assert!(provider.capability().nip46_permissions.is_empty());
assert_eq!(receipt.mode, RadrootsSdkSignerMode::LocalKey);
- assert_eq!(receipt.signer_pubkey, USER_PUBLIC_KEY_HEX);
+ assert_eq!(receipt.signer_pubkey, user_pubkey());
assert_eq!(receipt.signed_event_id, draft.expected_event_id_str());
assert_eq!(
progress,
@@ -370,13 +363,9 @@ fn signer_provider_reports_myc_status_capability_and_constructor_errors() {
];
let target = RadrootsNostrConnectClientTarget::new(remote_keys.public_key(), relays);
let transport = Arc::new(MockNip46Transport::new(remote_keys.clone(), Vec::new()));
- let signer = RadrootsSdkMycNip46Signer::new(
- client_keys(),
- target,
- USER_PUBLIC_KEY_HEX,
- transport.clone(),
- )
- .expect("signer");
+ let signer =
+ RadrootsSdkMycNip46Signer::new(client_keys(), target, user_pubkey(), transport.clone())
+ .expect("signer");
let provider = RadrootsSdkSignerProvider::MycNip46(Box::new(signer));
assert_eq!(provider.mode(), RadrootsSdkSignerMode::MycNip46);
@@ -385,7 +374,7 @@ fn signer_provider_reports_myc_status_capability_and_constructor_errors() {
RadrootsSdkSignerStatus {
mode: RadrootsSdkSignerMode::MycNip46,
state: RadrootsSdkSignerState::Ready,
- signer_pubkey: USER_PUBLIC_KEY_HEX.to_owned(),
+ signer_pubkey: user_pubkey().to_owned(),
remote_signer_pubkey: Some(remote_keys.public_key().to_hex()),
relay_count: 2,
}
@@ -394,7 +383,7 @@ fn signer_provider_reports_myc_status_capability_and_constructor_errors() {
provider.capability(),
RadrootsSdkSignerCapability {
mode: RadrootsSdkSignerMode::MycNip46,
- signer_pubkey: USER_PUBLIC_KEY_HEX.to_owned(),
+ signer_pubkey: user_pubkey().to_owned(),
remote_signer_pubkey: Some(remote_keys.public_key().to_hex()),
relays: vec![
"wss://relay-a.example.com".to_owned(),
@@ -420,9 +409,9 @@ fn signer_provider_reports_myc_status_capability_and_constructor_errors() {
#[test]
fn nip46_private_helpers_map_identity_adapter_and_response_edges() {
- let pubkey = USER_PUBLIC_KEY_HEX.parse().expect("pubkey");
+ let pubkey = user_pubkey().parse().expect("pubkey");
let identity = RadrootsSdkSignerIdentityOnly { pubkey };
- assert_eq!(identity.pubkey().as_str(), USER_PUBLIC_KEY_HEX);
+ assert_eq!(identity.pubkey().as_str(), user_pubkey());
assert!(matches!(
identity.sign_frozen_draft(&frozen_draft()),
Err(RadrootsSignerError::Unavailable)
@@ -516,7 +505,7 @@ async fn nip46_transport_adapter_delegates_publish_and_response_poll() {
fn myc_nip46_product_permissions_cover_sdk_write_event_kinds() {
let permissions = radroots_sdk_myc_nip46_product_permissions();
let rendered = radroots_sdk_myc_nip46_product_permission_strings();
- let mut expected_kinds = vec![KIND_FARM, KIND_LISTING];
+ let mut expected_kinds = vec![KIND_FARM, KIND_CLASSIFIED_LISTING];
expected_kinds.extend_from_slice(&TRADE_MUTATION_EVENT_KINDS);
assert_eq!(permissions.as_slice().len(), expected_kinds.len());
@@ -554,7 +543,7 @@ async fn myc_nip46_provider_signs_and_validates_remote_event() {
vec![nostr::RelayUrl::parse("wss://relay.example.com").expect("relay")],
);
let signer =
- RadrootsSdkMycNip46Signer::new(client_keys, target, USER_PUBLIC_KEY_HEX, transport.clone())
+ RadrootsSdkMycNip46Signer::new(client_keys, target, user_pubkey(), transport.clone())
.expect("signer");
let provider = RadrootsSdkSignerProvider::MycNip46(Box::new(signer));
assert_eq!(
@@ -577,7 +566,7 @@ async fn myc_nip46_provider_signs_and_validates_remote_event() {
.expect("receipt");
assert_eq!(receipt.mode, RadrootsSdkSignerMode::MycNip46);
- assert_eq!(receipt.signer_pubkey, USER_PUBLIC_KEY_HEX);
+ assert_eq!(receipt.signer_pubkey, user_pubkey());
assert_eq!(
receipt.remote_signer_pubkey,
Some(remote_keys.public_key().to_hex())
@@ -759,9 +748,8 @@ async fn myc_nip46_provider_reports_auth_challenge_progress_and_timeout() {
)],
));
let target = RadrootsNostrConnectClientTarget::new(remote_keys.public_key(), Vec::new());
- let signer =
- RadrootsSdkMycNip46Signer::new(client_keys, target, USER_PUBLIC_KEY_HEX, transport)
- .expect("signer");
+ let signer = RadrootsSdkMycNip46Signer::new(client_keys, target, user_pubkey(), transport)
+ .expect("signer");
let mut progress = Vec::new();
let draft = frozen_draft();
let actor = actor();
@@ -807,9 +795,8 @@ async fn myc_nip46_provider_returns_progress_sink_errors_from_auth_challenge() {
)],
));
let target = RadrootsNostrConnectClientTarget::new(remote_keys.public_key(), Vec::new());
- let signer =
- RadrootsSdkMycNip46Signer::new(client_keys, target, USER_PUBLIC_KEY_HEX, transport)
- .expect("signer");
+ let signer = RadrootsSdkMycNip46Signer::new(client_keys, target, user_pubkey(), transport)
+ .expect("signer");
let draft = frozen_draft();
let actor = actor();
let mut observed = Vec::new();
@@ -860,7 +847,7 @@ async fn myc_nip46_provider_rejects_zero_timeout_policy() {
let constructor_error = match RadrootsSdkMycNip46Signer::new_with_request_policy(
client_keys(),
target,
- USER_PUBLIC_KEY_HEX,
+ user_pubkey(),
transport,
RadrootsSdkMycNip46RequestPolicy {
request_timeout: Duration::ZERO,
@@ -886,7 +873,7 @@ async fn myc_nip46_provider_times_out_hanging_transport() {
let signer = RadrootsSdkMycNip46Signer::new_with_request_policy(
client_keys,
target,
- USER_PUBLIC_KEY_HEX,
+ user_pubkey(),
transport,
policy,
)
@@ -928,7 +915,7 @@ async fn myc_nip46_provider_rejects_returned_event_drift() {
user_keys(),
frozen_draft_with(
"radroots.farm.profile.v1",
- USER_PUBLIC_KEY_HEX,
+ user_pubkey(),
KIND_FARM,
1_700_000_000,
vec![vec!["d".to_owned(), "sdk-signer-id-drift".to_owned()]],
@@ -940,7 +927,7 @@ async fn myc_nip46_provider_rejects_returned_event_drift() {
user_keys(),
frozen_draft_with(
"radroots.farm.profile.v1",
- USER_PUBLIC_KEY_HEX,
+ user_pubkey(),
KIND_FARM,
1_700_000_001,
vec![vec!["d".to_owned(), "sdk-signer".to_owned()]],
@@ -952,7 +939,7 @@ async fn myc_nip46_provider_rejects_returned_event_drift() {
user_keys(),
frozen_draft_with(
"radroots.farm.coop.v1",
- USER_PUBLIC_KEY_HEX,
+ user_pubkey(),
KIND_COOP,
1_700_000_000,
vec![vec!["d".to_owned(), "sdk-signer".to_owned()]],
@@ -964,7 +951,7 @@ async fn myc_nip46_provider_rejects_returned_event_drift() {
user_keys(),
frozen_draft_with(
"radroots.farm.profile.v1",
- USER_PUBLIC_KEY_HEX,
+ user_pubkey(),
KIND_FARM,
1_700_000_000,
vec![vec!["d".to_owned(), "sdk-signer-tags-drift".to_owned()]],
@@ -976,7 +963,7 @@ async fn myc_nip46_provider_rejects_returned_event_drift() {
user_keys(),
frozen_draft_with(
"radroots.farm.profile.v1",
- USER_PUBLIC_KEY_HEX,
+ user_pubkey(),
KIND_FARM,
1_700_000_000,
vec![vec!["d".to_owned(), "sdk-signer".to_owned()]],
@@ -996,9 +983,8 @@ async fn myc_nip46_provider_rejects_returned_event_drift() {
)],
));
let target = RadrootsNostrConnectClientTarget::new(remote_keys.public_key(), Vec::new());
- let signer =
- RadrootsSdkMycNip46Signer::new(client_keys, target, USER_PUBLIC_KEY_HEX, transport)
- .expect("signer");
+ let signer = RadrootsSdkMycNip46Signer::new(client_keys, target, user_pubkey(), transport)
+ .expect("signer");
let actor = actor();
let error = signer
diff --git a/crates/sdk/tests/unit/sync_runtime_tests.rs b/crates/sdk/tests/unit/sync_runtime_tests.rs
@@ -30,9 +30,7 @@ use radroots_event::ids::RadrootsEventId;
use radroots_event::kinds::KIND_FARM;
use radroots_event_store::RadrootsEventStoreStatusSummary;
#[cfg(feature = "radrootsd-execution")]
-use radroots_nostr::prelude::{
- RadrootsNostrKeys, RadrootsNostrSecretKey, radroots_nostr_sign_frozen_draft,
-};
+use radroots_nostr::prelude::{RadrootsNostrKeys, radroots_nostr_sign_frozen_draft};
#[cfg(feature = "radrootsd-execution")]
use radroots_outbox::{
RadrootsOutboxClaimedEvent, RadrootsOutboxDeliveryPlanInput, RadrootsOutboxDeliveryPlanStatus,
@@ -65,14 +63,21 @@ use std::io::ErrorKind;
#[cfg(feature = "radrootsd-execution")]
use std::net::TcpListener;
#[cfg(feature = "radrootsd-execution")]
+use std::sync::LazyLock;
+#[cfg(feature = "radrootsd-execution")]
use std::time::Duration;
#[cfg(feature = "radrootsd-execution")]
-const RADROOTSD_FIXTURE_SIGNER_SECRET_KEY_HEX: &str =
- "10c5304d6c9ae3a1a16f7860f1cc8f5e3a76225a2663b3a989a0d775919b7df5";
+static RADROOTSD_FIXTURE_SIGNER_KEYS: LazyLock<RadrootsNostrKeys> =
+ LazyLock::new(RadrootsNostrKeys::generate);
#[cfg(feature = "radrootsd-execution")]
-const RADROOTSD_FIXTURE_SIGNER_PUBLIC_KEY_HEX: &str =
- "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df";
+static RADROOTSD_FIXTURE_SIGNER_PUBLIC_KEY: LazyLock<String> =
+ LazyLock::new(|| RADROOTSD_FIXTURE_SIGNER_KEYS.public_key().to_hex());
+
+#[cfg(feature = "radrootsd-execution")]
+fn radrootsd_fixture_signer_pubkey() -> &'static str {
+ RADROOTSD_FIXTURE_SIGNER_PUBLIC_KEY.as_str()
+}
struct UnusedPublishAdapter;
@@ -85,13 +90,10 @@ struct RadrootsdFixtureSigner {
#[cfg(feature = "radrootsd-execution")]
impl RadrootsdFixtureSigner {
fn new() -> Self {
- let secret_key = RadrootsNostrSecretKey::from_hex(RADROOTSD_FIXTURE_SIGNER_SECRET_KEY_HEX)
- .expect("secret key");
- let keys = RadrootsNostrKeys::new(secret_key);
Self {
- identity: RadrootsSignerIdentity::new(RADROOTSD_FIXTURE_SIGNER_PUBLIC_KEY_HEX)
+ identity: RadrootsSignerIdentity::new(radrootsd_fixture_signer_pubkey())
.expect("identity"),
- keys,
+ keys: RADROOTSD_FIXTURE_SIGNER_KEYS.clone(),
}
}
}
@@ -127,7 +129,7 @@ impl RadrootsRelayPublishAdapter for UnusedPublishAdapter {
#[cfg(feature = "radrootsd-execution")]
fn radrootsd_actor() -> RadrootsActorContext {
RadrootsActorContext::test(
- RADROOTSD_FIXTURE_SIGNER_PUBLIC_KEY_HEX,
+ radrootsd_fixture_signer_pubkey(),
[RadrootsActorRole::Farmer],
)
.expect("actor")
@@ -141,7 +143,7 @@ fn radrootsd_frozen_draft(d_tag: &str) -> RadrootsEventDraft {
1_700_000_000,
vec![vec!["d".to_owned(), d_tag.to_owned()]],
"{}",
- RADROOTSD_FIXTURE_SIGNER_PUBLIC_KEY_HEX,
+ radrootsd_fixture_signer_pubkey(),
)
.expect("frozen draft")
}
@@ -344,7 +346,7 @@ fn radrootsd_job(
terminal: !retryable,
delivery_satisfied,
event_id: event_id.to_owned(),
- pubkey: RADROOTSD_FIXTURE_SIGNER_PUBLIC_KEY_HEX.to_owned(),
+ pubkey: radrootsd_fixture_signer_pubkey().to_owned(),
event_kind: KIND_FARM,
target_policy: TransportPublishTargetPolicy::nostr(
NostrPublishTargetSourcePolicy::RequestThenAuthorWriteThenDaemonDefault,
@@ -531,14 +533,14 @@ fn auth_policy_defaults_and_outbox_state_mappings_cover_all_public_states() {
fn sync_status_summary_conversions_preserve_all_fields() {
let event_summary = RadrootsEventStoreStatusSummary {
total_events: 11,
- projection_eligible_events: 7,
+ valid_stream_events: 7,
transport_observations: 3,
last_event_seq: Some(9),
last_event_updated_at_ms: Some(1_700_000_000_000),
};
let event_status = SyncEventStoreStatus::from(event_summary);
assert_eq!(event_status.total_events, 11);
- assert_eq!(event_status.projection_eligible_events, 7);
+ assert_eq!(event_status.valid_stream_events, 7);
assert_eq!(event_status.transport_observations, 3);
assert_eq!(event_status.last_event_seq, Some(9));
assert_eq!(
@@ -1104,7 +1106,7 @@ async fn radrootsd_push_reports_missing_signed_claim_before_daemon_publish() {
1_700_000_000,
vec![vec!["d".to_owned(), "missing-signed-event".to_owned()]],
"{}",
- RADROOTSD_FIXTURE_SIGNER_PUBLIC_KEY_HEX,
+ radrootsd_fixture_signer_pubkey(),
)
.expect("draft"),
signed_event: None,
diff --git a/crates/sdk/tests/unit/trade_runtime_tests.rs b/crates/sdk/tests/unit/trade_runtime_tests.rs
@@ -7,7 +7,7 @@ use radroots_authority::{RadrootsActorContext, RadrootsLocalEventSigner};
use radroots_event::{
contract::RadrootsActorRole,
ids::{
- RadrootsAddressableCoordinate, RadrootsDTag, RadrootsEventId, RadrootsInventoryBinId,
+ RadrootsClassifiedListingAddress, RadrootsDTag, RadrootsEventId, RadrootsInventoryBinId,
RadrootsPublicKey, RadrootsTradeId,
},
kinds::TRADE_MUTATION_EVENT_KINDS,
@@ -22,17 +22,12 @@ use radroots_event::{
RadrootsTradeMutationEnvelopeV1, canonical_trade_mutation_content,
},
};
-use radroots_nostr::prelude::{RadrootsNostrKeys, RadrootsNostrSecretKey};
+use radroots_nostr::prelude::RadrootsNostrKeys;
use radroots_trade::workflow::{
RADROOTS_TRADE_REDUCER_CONTRACT_ID, RADROOTS_TRADE_REDUCER_VERSION,
RadrootsTradePrivateTermsStateV1,
};
-const BUYER_SECRET_KEY_HEX: &str =
- "10c5304d6c9ae3a1a16f7860f1cc8f5e3a76225a2663b3a989a0d775919b7df5";
-const SELLER_SECRET_KEY_HEX: &str =
- "59392e9068f66431b12f70218fb61281cb6b433d7f27c55d61f1a63fe1a96ff8";
-
fn pubkey(value: &str) -> RadrootsPublicKey {
RadrootsPublicKey::parse(value).expect("pubkey")
}
@@ -45,9 +40,8 @@ fn trade_id() -> RadrootsTradeId {
RadrootsTradeId::parse("11111111111111111111111111111111").expect("trade id")
}
-fn local_signer(secret_key_hex: &str) -> (String, RadrootsLocalEventSigner) {
- let secret_key = RadrootsNostrSecretKey::from_hex(secret_key_hex).expect("secret key");
- let keys = RadrootsNostrKeys::new(secret_key);
+fn local_signer() -> (String, RadrootsLocalEventSigner) {
+ let keys = RadrootsNostrKeys::generate();
let pubkey = keys.public_key().to_hex();
(
pubkey,
@@ -135,7 +129,7 @@ fn candidate(buyer_pubkey: &str, seller_pubkey: &str) -> RadrootsTradeCandidateT
farm_id: RadrootsDTag::parse("farm-1").expect("farm id"),
lines: vec![RadrootsTradeCandidateLineV1 {
line_id: RadrootsDTag::parse("line-1").expect("line id"),
- listing_addr: RadrootsAddressableCoordinate::parse(format!(
+ listing_addr: RadrootsClassifiedListingAddress::parse(format!(
"30402:{seller_pubkey}:listing-1"
))
.expect("listing address"),
@@ -268,8 +262,8 @@ fn accepted_decision(
#[tokio::test]
async fn trade_commands_query_and_private_terms_are_release_product_v1() {
- let (buyer_pubkey, buyer_signer) = local_signer(BUYER_SECRET_KEY_HEX);
- let (seller_pubkey, seller_signer) = local_signer(SELLER_SECRET_KEY_HEX);
+ let (buyer_pubkey, buyer_signer) = local_signer();
+ let (seller_pubkey, seller_signer) = local_signer();
let sdk = RadrootsClient::builder()
.fixed_clock(RadrootsSdkTimestamp::from_unix_seconds(1_799_000_100))
.build()
diff --git a/crates/sdk/tests/unit/workflow_runtime_tests.rs b/crates/sdk/tests/unit/workflow_runtime_tests.rs
@@ -4,15 +4,24 @@ use crate::{RadrootsSdkLocalKeySigner, RadrootsSdkSignerProvider};
use radroots_authority::{RadrootsSignerError, RadrootsSignerIdentity};
use radroots_event::contract::RadrootsActorRole;
use radroots_event::draft::{RadrootsEventDraft, RadrootsSignedEvent, RadrootsSignedEventParts};
-use radroots_event::kinds::KIND_FARM;
-use radroots_nostr::prelude::{
- RadrootsNostrKeys, RadrootsNostrSecretKey, radroots_nostr_sign_frozen_draft,
-};
+use radroots_event::kinds::{KIND_FARM, KIND_GEOCHAT};
+use radroots_nostr::prelude::{RadrootsNostrKeys, radroots_nostr_sign_frozen_draft};
+use std::sync::LazyLock;
-const FARMER_SECRET_KEY_HEX: &str =
- "10c5304d6c9ae3a1a16f7860f1cc8f5e3a76225a2663b3a989a0d775919b7df5";
-const FARMER_PUBLIC_KEY_HEX: &str =
- "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df";
+struct WorkflowKeyMaterial {
+ keys: RadrootsNostrKeys,
+ pubkey: String,
+}
+
+static WORKFLOW_KEY_MATERIAL: LazyLock<WorkflowKeyMaterial> = LazyLock::new(|| {
+ let keys = RadrootsNostrKeys::generate();
+ let pubkey = keys.public_key().to_hex();
+ WorkflowKeyMaterial { keys, pubkey }
+});
+
+fn farmer_pubkey() -> &'static str {
+ WORKFLOW_KEY_MATERIAL.pubkey.as_str()
+}
fn workflow_idempotency_key(index: u16) -> SdkIdempotencyKey {
SdkIdempotencyKey::new(format!("01890f0e-6c00-7000-8000-00000000{index:04x}"))
@@ -26,16 +35,68 @@ struct WorkflowSigner {
impl WorkflowSigner {
fn new() -> Self {
- let secret_key =
- RadrootsNostrSecretKey::from_hex(FARMER_SECRET_KEY_HEX).expect("secret key");
- let keys = RadrootsNostrKeys::new(secret_key);
Self {
- identity: RadrootsSignerIdentity::new(FARMER_PUBLIC_KEY_HEX).expect("identity"),
- keys,
+ identity: RadrootsSignerIdentity::new(farmer_pubkey()).expect("identity"),
+ keys: WORKFLOW_KEY_MATERIAL.keys.clone(),
+ }
+ }
+}
+
+struct FailIfCalledSigner {
+ identity: RadrootsSignerIdentity,
+}
+
+impl FailIfCalledSigner {
+ fn new() -> Self {
+ Self {
+ identity: RadrootsSignerIdentity::new(farmer_pubkey()).expect("identity"),
}
}
}
+impl RadrootsEventSigner for FailIfCalledSigner {
+ fn pubkey(&self) -> &radroots_event::ids::RadrootsPublicKey {
+ self.identity.pubkey()
+ }
+
+ fn sign_frozen_draft(
+ &self,
+ _draft: &RadrootsEventDraft,
+ ) -> Result<RadrootsSignedEvent, RadrootsSignerError> {
+ panic!("ephemeral workflow preflight must not invoke the signer")
+ }
+}
+
+struct InvalidSignatureSigner(WorkflowSigner);
+
+impl InvalidSignatureSigner {
+ fn new() -> Self {
+ Self(WorkflowSigner::new())
+ }
+}
+
+impl RadrootsEventSigner for InvalidSignatureSigner {
+ fn pubkey(&self) -> &radroots_event::ids::RadrootsPublicKey {
+ self.0.pubkey()
+ }
+
+ fn sign_frozen_draft(
+ &self,
+ draft: &RadrootsEventDraft,
+ ) -> Result<RadrootsSignedEvent, RadrootsSignerError> {
+ let signed = self.0.sign_frozen_draft(draft)?;
+ let mut wire = signed.wire().clone();
+ wire.sig = "0".repeat(128);
+ let raw_json =
+ serde_json::to_string(&wire).expect("invalid-signature fixture must serialize");
+ RadrootsSignedEvent::from_wire_verified_id(wire, raw_json).map_err(|error| {
+ RadrootsSignerError::SigningFailed {
+ message: error.to_string(),
+ }
+ })
+ }
+}
+
impl RadrootsEventSigner for WorkflowSigner {
fn pubkey(&self) -> &radroots_event::ids::RadrootsPublicKey {
self.identity.pubkey()
@@ -73,6 +134,18 @@ fn frozen_draft() -> RadrootsEventDraft {
frozen_draft_for("a".repeat(64).as_str())
}
+fn ephemeral_draft_for(pubkey: &str) -> RadrootsEventDraft {
+ RadrootsEventDraft::new(
+ "radroots.social.geochat.v1",
+ KIND_GEOCHAT,
+ 1_700_000_000,
+ Vec::new(),
+ "Transient local message",
+ pubkey,
+ )
+ .expect("ephemeral draft")
+}
+
fn signed_event() -> RadrootsSignedEvent {
let draft = frozen_draft();
let sig = "c".repeat(128);
@@ -157,6 +230,30 @@ fn workflow_digest_and_event_helpers_cover_error_and_input_paths() {
"wss://relay.example.com"
);
assert!(input.event_store_inserted);
+ assert_eq!(
+ durable_event_persistence(
+ draft.expected_event_id_str(),
+ &RadrootsEventPersistence::Inserted { seq: 7 },
+ )
+ .expect("inserted persistence"),
+ (true, 7)
+ );
+ assert_eq!(
+ durable_event_persistence(
+ draft.expected_event_id_str(),
+ &RadrootsEventPersistence::Duplicate { seq: 7 },
+ )
+ .expect("duplicate persistence"),
+ (false, 7)
+ );
+ assert!(matches!(
+ durable_event_persistence(
+ draft.expected_event_id_str(),
+ &RadrootsEventPersistence::NotPersisted,
+ ),
+ Err(RadrootsSdkError::InvalidRequest { message })
+ if message.contains("requires durable local event-store persistence")
+ ));
let frozen = frozen_draft_json(&draft).expect("frozen draft json");
assert!(frozen.contains("\"expected_event_id\""));
let receipt = SdkWorkflowEnqueueReceipt {
@@ -175,6 +272,133 @@ fn workflow_digest_and_event_helpers_cover_error_and_input_paths() {
}
#[tokio::test]
+async fn enqueue_signed_workflow_rejects_ephemeral_event_before_durable_commit() {
+ let sdk = crate::RadrootsClient::builder()
+ .fixed_clock(crate::RadrootsSdkTimestamp::from_unix_seconds(
+ 1_700_000_013,
+ ))
+ .build()
+ .await
+ .expect("sdk");
+ let actor =
+ RadrootsActorContext::test(farmer_pubkey(), [RadrootsActorRole::Farmer]).expect("actor");
+ let draft = ephemeral_draft_for(farmer_pubkey());
+ let error = enqueue_signed_workflow(
+ &sdk,
+ SdkWorkflowEnqueueRequest {
+ operation_kind: "workflow.ephemeral.test.v1",
+ actor: &actor,
+ frozen_draft: &draft,
+ target_policy: TargetPolicy::LocalOnly,
+ satisfaction_policy: SatisfactionPolicy::NoWait,
+ idempotency_key: Some(workflow_idempotency_key(0x247)),
+ },
+ &FailIfCalledSigner::new(),
+ )
+ .await
+ .expect_err("ephemeral workflow");
+
+ assert!(matches!(
+ error,
+ RadrootsSdkError::InvalidRequest { ref message }
+ if message.contains("cannot enqueue ephemeral event kind")
+ ));
+ assert!(!error.retryable());
+ assert_eq!(
+ sdk._event_store
+ .status_summary()
+ .await
+ .expect("event store status")
+ .total_events,
+ 0
+ );
+ assert_eq!(
+ sdk._outbox
+ .status_summary(0)
+ .await
+ .expect("outbox status")
+ .total_events,
+ 0
+ );
+ let journal_count: i64 = sqlx::query_scalar(
+ "SELECT COUNT(*) FROM sdk_runtime_operation_journal WHERE operation_kind = ?",
+ )
+ .bind("workflow.ephemeral.test.v1")
+ .fetch_one(sdk._event_store.pool())
+ .await
+ .expect("journal count");
+ assert_eq!(journal_count, 0);
+}
+
+#[tokio::test]
+async fn enqueue_signed_workflow_rejects_invalid_signer_signature_without_storage_mutation() {
+ let sdk = crate::RadrootsClient::builder()
+ .fixed_clock(crate::RadrootsSdkTimestamp::from_unix_seconds(
+ 1_700_000_013,
+ ))
+ .build()
+ .await
+ .expect("sdk");
+ let actor =
+ RadrootsActorContext::test(farmer_pubkey(), [RadrootsActorRole::Farmer]).expect("actor");
+ let draft = frozen_draft_for_d_tag(farmer_pubkey(), "workflow-invalid-signature");
+ let operation_kind = "workflow.invalid-signature.test.v1";
+
+ let error = enqueue_signed_workflow(
+ &sdk,
+ SdkWorkflowEnqueueRequest {
+ operation_kind,
+ actor: &actor,
+ frozen_draft: &draft,
+ target_policy: TargetPolicy::LocalOnly,
+ satisfaction_policy: SatisfactionPolicy::NoWait,
+ idempotency_key: Some(workflow_idempotency_key(0x248)),
+ },
+ &InvalidSignatureSigner::new(),
+ )
+ .await
+ .expect_err("invalid signer signature");
+
+ assert!(matches!(
+ error,
+ RadrootsSdkError::SignerReturnedEventDrift {
+ ref operation,
+ ref reason,
+ } if operation == operation_kind
+ && reason.contains("failed NIP-01 verification")
+ ));
+ assert!(!error.retryable());
+ assert_eq!(
+ error.recovery_actions(),
+ vec![crate::RadrootsSdkRecoveryAction::ConfigureSigner]
+ );
+ assert_eq!(
+ sdk._event_store
+ .status_summary()
+ .await
+ .expect("event store status")
+ .total_events,
+ 0
+ );
+ assert_eq!(
+ sdk._outbox
+ .status_summary(0)
+ .await
+ .expect("outbox status")
+ .total_events,
+ 0
+ );
+ let journal_state: String = sqlx::query_scalar(
+ "SELECT state FROM sdk_runtime_operation_journal WHERE operation_kind = ?",
+ )
+ .bind(operation_kind)
+ .fetch_one(sdk._event_store.pool())
+ .await
+ .expect("journal state");
+ assert_eq!(journal_state, "rejected");
+}
+
+#[tokio::test]
async fn workflow_idempotency_replays_original_receipt_and_conflicts_on_new_command_hash() {
let sdk = crate::RadrootsClient::builder()
.fixed_clock(crate::RadrootsSdkTimestamp::from_unix_seconds(
@@ -183,10 +407,10 @@ async fn workflow_idempotency_replays_original_receipt_and_conflicts_on_new_comm
.build()
.await
.expect("sdk");
- let actor = RadrootsActorContext::test(FARMER_PUBLIC_KEY_HEX, [RadrootsActorRole::Farmer])
- .expect("actor");
+ let actor =
+ RadrootsActorContext::test(farmer_pubkey(), [RadrootsActorRole::Farmer]).expect("actor");
let signer = WorkflowSigner::new();
- let draft = frozen_draft_for_d_tag(FARMER_PUBLIC_KEY_HEX, "workflow-target-policy");
+ let draft = frozen_draft_for_d_tag(farmer_pubkey(), "workflow-target-policy");
let first_target_policy = TargetPolicy::try_nostr_relays(
["wss://relay-a.example.com"],
crate::NostrRelayUrlPolicy::Public,
@@ -298,10 +522,10 @@ async fn enqueue_signed_workflow_maps_no_wait_directly_and_allows_local_only_pro
.build()
.await
.expect("sdk");
- let actor = RadrootsActorContext::test(FARMER_PUBLIC_KEY_HEX, [RadrootsActorRole::Farmer])
- .expect("actor");
+ let actor =
+ RadrootsActorContext::test(farmer_pubkey(), [RadrootsActorRole::Farmer]).expect("actor");
let signer = WorkflowSigner::new();
- let draft = frozen_draft_for_d_tag(FARMER_PUBLIC_KEY_HEX, "workflow-no-wait");
+ let draft = frozen_draft_for_d_tag(farmer_pubkey(), "workflow-no-wait");
let receipt = enqueue_signed_workflow(
&sdk,
@@ -372,9 +596,9 @@ async fn enqueue_signed_workflow_rejects_missing_explicit_idempotency_key_withou
.build()
.await
.expect("sdk");
- let actor = RadrootsActorContext::test(FARMER_PUBLIC_KEY_HEX, [RadrootsActorRole::Farmer])
- .expect("actor");
- let draft = frozen_draft_for_d_tag(FARMER_PUBLIC_KEY_HEX, "workflow-missing-idempotency");
+ let actor =
+ RadrootsActorContext::test(farmer_pubkey(), [RadrootsActorRole::Farmer]).expect("actor");
+ let draft = frozen_draft_for_d_tag(farmer_pubkey(), "workflow-missing-idempotency");
let error = match enqueue_signed_workflow(
&sdk,
@@ -423,10 +647,10 @@ async fn enqueue_signed_workflow_stores_signed_event_and_reports_idempotency_con
.build()
.await
.expect("sdk");
- let actor = RadrootsActorContext::test(FARMER_PUBLIC_KEY_HEX, [RadrootsActorRole::Farmer])
- .expect("actor");
+ let actor =
+ RadrootsActorContext::test(farmer_pubkey(), [RadrootsActorRole::Farmer]).expect("actor");
let signer = WorkflowSigner::new();
- let first_draft = frozen_draft_for_d_tag(FARMER_PUBLIC_KEY_HEX, "workflow-success");
+ let first_draft = frozen_draft_for_d_tag(farmer_pubkey(), "workflow-success");
let idempotency_key =
SdkIdempotencyKey::new("01890f0e-6c00-7000-8000-000000000237").expect("idempotency");
let receipt = enqueue_signed_workflow(
@@ -469,7 +693,7 @@ async fn enqueue_signed_workflow_stores_signed_event_and_reports_idempotency_con
1
);
- let second_draft = frozen_draft_for_d_tag(FARMER_PUBLIC_KEY_HEX, "workflow-conflict");
+ let second_draft = frozen_draft_for_d_tag(farmer_pubkey(), "workflow-conflict");
let error = match enqueue_signed_workflow(
&sdk,
SdkWorkflowEnqueueRequest {
@@ -528,9 +752,9 @@ async fn enqueue_configured_signed_workflow_uses_sdk_signer_provider() {
.build()
.await
.expect("sdk");
- let actor = RadrootsActorContext::test(FARMER_PUBLIC_KEY_HEX, [RadrootsActorRole::Farmer])
- .expect("actor");
- let draft = frozen_draft_for_d_tag(FARMER_PUBLIC_KEY_HEX, "workflow-configured");
+ let actor =
+ RadrootsActorContext::test(farmer_pubkey(), [RadrootsActorRole::Farmer]).expect("actor");
+ let draft = frozen_draft_for_d_tag(farmer_pubkey(), "workflow-configured");
let receipt = enqueue_configured_signed_workflow(
&sdk,
@@ -569,9 +793,9 @@ async fn enqueue_signed_workflow_reports_runtime_pool_failure_before_mutation()
0
);
sdk._outbox.pool().close().await;
- let actor = RadrootsActorContext::test(FARMER_PUBLIC_KEY_HEX, [RadrootsActorRole::Farmer])
- .expect("actor");
- let draft = frozen_draft_for(FARMER_PUBLIC_KEY_HEX);
+ let actor =
+ RadrootsActorContext::test(farmer_pubkey(), [RadrootsActorRole::Farmer]).expect("actor");
+ let draft = frozen_draft_for(farmer_pubkey());
let request = SdkWorkflowEnqueueRequest {
operation_kind: "workflow.test.v1",
actor: &actor,
@@ -591,9 +815,9 @@ async fn enqueue_signed_workflow_reports_runtime_pool_failure_before_mutation()
#[tokio::test]
async fn enqueue_signed_workflow_reports_store_failures() {
- let actor = RadrootsActorContext::test(FARMER_PUBLIC_KEY_HEX, [RadrootsActorRole::Farmer])
- .expect("actor");
- let draft = frozen_draft_for(FARMER_PUBLIC_KEY_HEX);
+ let actor =
+ RadrootsActorContext::test(farmer_pubkey(), [RadrootsActorRole::Farmer]).expect("actor");
+ let draft = frozen_draft_for(farmer_pubkey());
let closed_store_sdk = crate::RadrootsClient::builder()
.transport_profile(nostr_profile("wss://relay.example.com"))
.build()
@@ -627,9 +851,9 @@ async fn enqueue_signed_workflow_reports_clock_failures() {
.build()
.await
.expect("sdk");
- let actor = RadrootsActorContext::test(FARMER_PUBLIC_KEY_HEX, [RadrootsActorRole::Farmer])
- .expect("actor");
- let draft = frozen_draft_for(FARMER_PUBLIC_KEY_HEX);
+ let actor =
+ RadrootsActorContext::test(farmer_pubkey(), [RadrootsActorRole::Farmer]).expect("actor");
+ let draft = frozen_draft_for(farmer_pubkey());
let request = SdkWorkflowEnqueueRequest {
operation_kind: "workflow.test.v1",
actor: &actor,
@@ -647,9 +871,9 @@ async fn enqueue_signed_workflow_reports_clock_failures() {
#[tokio::test]
async fn enqueue_signed_workflow_rejects_transport_profile_targets_without_radrootsd_execution() {
let sdk = crate::RadrootsClient::builder().build().await.expect("sdk");
- let actor = RadrootsActorContext::test(FARMER_PUBLIC_KEY_HEX, [RadrootsActorRole::Farmer])
- .expect("actor");
- let draft = frozen_draft_for(FARMER_PUBLIC_KEY_HEX);
+ let actor =
+ RadrootsActorContext::test(farmer_pubkey(), [RadrootsActorRole::Farmer]).expect("actor");
+ let draft = frozen_draft_for(farmer_pubkey());
let request = SdkWorkflowEnqueueRequest {
operation_kind: "workflow.test.v1",
actor: &actor,
diff --git a/crates/trade_bindings/src/dto.rs b/crates/trade_bindings/src/dto.rs
@@ -3,7 +3,7 @@ use dto_bindgen_core::{
RustTypeId, SourceSpan, TargetFieldNames, TargetOverride, TypeDef, TypeRef, VariantDef,
VariantShape, WireFieldNames,
};
-use radroots_trade::listing::model::RadrootsTradeListingTotal;
+use radroots_trade::operational_listing::model::RadrootsOperationalListingTotal;
pub fn dto_roots() -> Vec<RootDescriptor> {
let mut roots = radroots_trade::dto::dto_roots()
@@ -52,22 +52,30 @@ imported_ts_type!(RadrootsCoreQuantityImport, "RadrootsCoreQuantity");
imported_ts_type!(RadrootsCoreQuantityPriceImport, "RadrootsCoreQuantityPrice");
imported_ts_type!(RadrootsCoreUnitImport, "RadrootsCoreUnit");
imported_ts_type!(RadrootsFarmRefImport, "RadrootsFarmRef");
-imported_ts_type!(RadrootsListingImport, "RadrootsListing");
imported_ts_type!(
- RadrootsListingAvailabilityImport,
- "RadrootsListingAvailability"
+ RadrootsOperationalListingAvailabilityImport,
+ "RadrootsOperationalListingAvailability"
);
-imported_ts_type!(RadrootsListingBinImport, "RadrootsListingBin");
imported_ts_type!(
- RadrootsListingDeliveryMethodImport,
- "RadrootsListingDeliveryMethod"
+ RadrootsOperationalListingBinImport,
+ "RadrootsOperationalListingBin"
);
-imported_ts_type!(RadrootsListingImageImport, "RadrootsListingImage");
imported_ts_type!(
- RadrootsListingPublicLocationImport,
- "RadrootsListingPublicLocation"
+ RadrootsOperationalListingDeliveryMethodImport,
+ "RadrootsOperationalListingDeliveryMethod"
+);
+imported_ts_type!(
+ RadrootsOperationalListingImageImport,
+ "RadrootsOperationalListingImage"
+);
+imported_ts_type!(
+ RadrootsOperationalListingPublicLocationImport,
+ "RadrootsOperationalListingPublicLocation"
+);
+imported_ts_type!(
+ RadrootsOperationalListingProductImport,
+ "RadrootsOperationalListingProduct"
);
-imported_ts_type!(RadrootsListingProductImport, "RadrootsListingProduct");
imported_ts_type!(RadrootsPlotRefImport, "RadrootsPlotRef");
imported_ts_type!(RadrootsResourceAreaRefImport, "RadrootsResourceAreaRef");
@@ -102,8 +110,8 @@ pub struct RadrootsTradeListingBackofficeView {
#[derive(dto_bindgen::Dto)]
pub struct RadrootsTradeListingBinProjection {
- pub bin: RadrootsListingBinImport,
- pub one_bin_total: RadrootsTradeListingTotal,
+ pub bin: RadrootsOperationalListingBinImport,
+ pub one_bin_total: RadrootsOperationalListingTotal,
}
#[derive(dto_bindgen::Dto)]
@@ -178,17 +186,17 @@ pub struct RadrootsTradeListingProjection {
pub seller_pubkey: String,
pub listing_id: String,
pub farm: RadrootsFarmRefImport,
- pub product: RadrootsListingProductImport,
+ pub product: RadrootsOperationalListingProductImport,
pub primary_bin_id: String,
pub bins: Vec<RadrootsTradeListingBinProjection>,
pub resource_area: Option<RadrootsResourceAreaRefImport>,
pub plot: Option<RadrootsPlotRefImport>,
pub discounts: Option<Vec<RadrootsCoreDiscountImport>>,
pub inventory_available: Option<RadrootsCoreDecimalImport>,
- pub availability: Option<RadrootsListingAvailabilityImport>,
- pub delivery_method: Option<RadrootsListingDeliveryMethodImport>,
- pub location: Option<RadrootsListingPublicLocationImport>,
- pub images: Option<Vec<RadrootsListingImageImport>>,
+ pub availability: Option<RadrootsOperationalListingAvailabilityImport>,
+ pub delivery_method: Option<RadrootsOperationalListingDeliveryMethodImport>,
+ pub location: Option<RadrootsOperationalListingPublicLocationImport>,
+ pub images: Option<Vec<RadrootsOperationalListingImageImport>>,
pub trade_count: u32,
pub open_trade_count: u32,
pub terminal_trade_count: u32,
@@ -243,7 +251,7 @@ pub struct RadrootsTradeMarketplaceListingSummary {
pub inventory_available: Option<RadrootsCoreDecimalImport>,
pub primary_bin_id: String,
pub primary_bin_label: Option<String>,
- pub primary_bin_total: RadrootsTradeListingTotal,
+ pub primary_bin_total: RadrootsOperationalListingTotal,
pub trade_count: u32,
pub open_trade_count: u32,
pub terminal_trade_count: u32,
diff --git a/crates/trade_bindings/src/lib.rs b/crates/trade_bindings/src/lib.rs
@@ -32,20 +32,20 @@ pub struct TradeLargeIntegerPolicyEntry {
pub const TRADE_TYPE_INVENTORY: &[TradeTypeInventoryEntry] = &[
event_import("RadrootsFarmRef"),
- event_import("RadrootsListing"),
- event_import("RadrootsListingAvailability"),
- event_import("RadrootsListingBin"),
- event_import("RadrootsListingDeliveryMethod"),
- event_import("RadrootsListingProduct"),
- event_import("RadrootsListingPublicLocation"),
- event_import("RadrootsListingStatus"),
+ event_import("RadrootsOperationalListing"),
+ event_import("RadrootsOperationalListingAvailability"),
+ event_import("RadrootsOperationalListingBin"),
+ event_import("RadrootsOperationalListingDeliveryMethod"),
+ event_import("RadrootsOperationalListingProduct"),
+ event_import("RadrootsOperationalListingPublicLocation"),
+ event_import("RadrootsOperationalListingStatus"),
local_shape("RadrootsTradeFacetCount"),
source_root("RadrootsTradeAgreementStateV1"),
source_root("RadrootsTradeAttestationStateV1"),
source_root("RadrootsTradeConflictStateV1"),
source_root("RadrootsTradeEvidenceStateV1"),
source_root("RadrootsTradeFulfillmentStateV1"),
- source_root("RadrootsTradeListing"),
+ source_root("RadrootsOperationalListingTradeProjection"),
local_shape("RadrootsTradeListingBackofficeOverlay"),
local_shape("RadrootsTradeListingBackofficeQuery"),
local_shape("RadrootsTradeListingBackofficeView"),
@@ -56,8 +56,8 @@ pub const TRADE_TYPE_INVENTORY: &[TradeTypeInventoryEntry] = &[
local_shape("RadrootsTradeListingQuery"),
local_shape("RadrootsTradeListingSort"),
local_shape("RadrootsTradeListingSortField"),
- source_root("RadrootsTradeListingSubtotal"),
- source_root("RadrootsTradeListingTotal"),
+ source_root("RadrootsOperationalListingSubtotal"),
+ source_root("RadrootsOperationalListingTotal"),
local_shape("RadrootsTradeMarketplaceListingSummary"),
local_shape("RadrootsTradeModerationFlag"),
local_shape("RadrootsTradeModerationSeverity"),
@@ -149,20 +149,20 @@ mod tests {
expected,
[
"RadrootsFarmRef",
- "RadrootsListing",
- "RadrootsListingAvailability",
- "RadrootsListingBin",
- "RadrootsListingDeliveryMethod",
- "RadrootsListingProduct",
- "RadrootsListingPublicLocation",
- "RadrootsListingStatus",
+ "RadrootsOperationalListing",
+ "RadrootsOperationalListingAvailability",
+ "RadrootsOperationalListingBin",
+ "RadrootsOperationalListingDeliveryMethod",
+ "RadrootsOperationalListingProduct",
+ "RadrootsOperationalListingPublicLocation",
+ "RadrootsOperationalListingStatus",
"RadrootsTradeFacetCount",
"RadrootsTradeAgreementStateV1",
"RadrootsTradeAttestationStateV1",
"RadrootsTradeConflictStateV1",
"RadrootsTradeEvidenceStateV1",
"RadrootsTradeFulfillmentStateV1",
- "RadrootsTradeListing",
+ "RadrootsOperationalListingTradeProjection",
"RadrootsTradeListingBackofficeOverlay",
"RadrootsTradeListingBackofficeQuery",
"RadrootsTradeListingBackofficeView",
@@ -173,8 +173,8 @@ mod tests {
"RadrootsTradeListingQuery",
"RadrootsTradeListingSort",
"RadrootsTradeListingSortField",
- "RadrootsTradeListingSubtotal",
- "RadrootsTradeListingTotal",
+ "RadrootsOperationalListingSubtotal",
+ "RadrootsOperationalListingTotal",
"RadrootsTradeMarketplaceListingSummary",
"RadrootsTradeModerationFlag",
"RadrootsTradeModerationSeverity",
@@ -195,13 +195,13 @@ mod tests {
fn source_owned_trade_support_types_are_marked_for_event_import() {
for export_name in [
"RadrootsFarmRef",
- "RadrootsListing",
- "RadrootsListingAvailability",
- "RadrootsListingBin",
- "RadrootsListingDeliveryMethod",
- "RadrootsListingProduct",
- "RadrootsListingPublicLocation",
- "RadrootsListingStatus",
+ "RadrootsOperationalListing",
+ "RadrootsOperationalListingAvailability",
+ "RadrootsOperationalListingBin",
+ "RadrootsOperationalListingDeliveryMethod",
+ "RadrootsOperationalListingProduct",
+ "RadrootsOperationalListingPublicLocation",
+ "RadrootsOperationalListingStatus",
] {
assert_eq!(
disposition(export_name),
@@ -226,9 +226,9 @@ mod tests {
"RadrootsTradeConflictStateV1",
"RadrootsTradeEvidenceStateV1",
"RadrootsTradeFulfillmentStateV1",
- "RadrootsTradeListing",
- "RadrootsTradeListingSubtotal",
- "RadrootsTradeListingTotal",
+ "RadrootsOperationalListingTradeProjection",
+ "RadrootsOperationalListingSubtotal",
+ "RadrootsOperationalListingTotal",
"RadrootsTradeNegotiationStateV1",
"RadrootsTradePaymentStateV1",
"RadrootsTradePrivateTermsStateV1",
diff --git a/packages/event-bindings/src/generated/constants.ts b/packages/event-bindings/src/generated/constants.ts
@@ -1,5 +1,5 @@
// @generated by cargo xtask generate ts
// Do not edit by hand.
-import type { RadrootsListingProductTagKeys } from "./types.js";
+import type { RadrootsOperationalListingProductTagKeys } from "./types.js";
-export const RADROOTS_LISTING_PRODUCT_TAG_KEYS: RadrootsListingProductTagKeys = ["key", "title", "category", "summary", "process", "lot", "location", "profile", "year"];
+export const RADROOTS_OPERATIONAL_LISTING_PRODUCT_TAG_KEYS: RadrootsOperationalListingProductTagKeys = ["key", "title", "category", "summary", "process", "lot", "location", "profile", "year"];
diff --git a/packages/event-bindings/src/generated/kinds.ts b/packages/event-bindings/src/generated/kinds.ts
@@ -104,7 +104,7 @@ export const KIND_ACCOUNT_CLAIM = 30380;
export const KIND_APP_DATA = 30078;
-export const KIND_LISTING = 30402;
+export const KIND_CLASSIFIED_LISTING = 30402;
export const KIND_APPLICATION_HANDLER = 31990;
diff --git a/packages/event-bindings/src/generated/types.ts b/packages/event-bindings/src/generated/types.ts
@@ -14,8 +14,6 @@ export type RadrootsAddressableRef = { kind: number, pubkey: string, d_tag: stri
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<RadrootsEventRef>, summary: string, evidence_refs: Array<RadrootsEventRef>, };
@@ -108,37 +106,39 @@ export type RadrootsListEntry = { tag: string, values: Array<string>, };
export type RadrootsListSet = { d_tag: string, content: string, entries: Array<RadrootsListEntry>, title?: string | null, description?: string | null, image?: string | null, };
-export type RadrootsListing = { d_tag: string, published_at?: string | null, farm?: RadrootsFarmRef, product: RadrootsListingProduct, primary_bin_id: string, bins: Array<RadrootsListingBin>, resource_area?: RadrootsResourceAreaRef | null, plot?: RadrootsPlotRef | null, discounts?: Array<RadrootsCoreDiscount> | null, inventory_available?: string | null, availability?: RadrootsListingAvailability | null, delivery_method?: RadrootsListingDeliveryMethod | null, location?: RadrootsListingPublicLocation | null, images?: Array<RadrootsListingImage> | null, };
+export type RadrootsMessage = { recipients: Array<RadrootsMessageRecipient>, content: string, reply_to?: RadrootsEventPtr | null, subject?: string | null, };
-export type RadrootsListingAvailability = { kind: "window", amount: { start?: string | null, end?: string | null, }, } | { kind: "status", amount: { status: RadrootsListingStatus, }, };
+export type RadrootsMessageFile = { recipients: Array<RadrootsMessageRecipient>, file_url: string, reply_to?: RadrootsEventPtr | null, subject?: string | null, file_type: string, encryption_algorithm: string, decryption_key: string, decryption_nonce: string, encrypted_hash: string, original_hash?: string | null, size?: string | null, dimensions?: RadrootsMessageFileDimensions | null, blurhash?: string | null, thumb?: string | null, fallbacks: Array<string>, };
-export type RadrootsListingBin = { bin_id: string, quantity: RadrootsCoreQuantity, price_per_canonical_unit: RadrootsCoreQuantityPrice, display_amount?: string | null, display_unit?: RadrootsCoreUnit | null, display_label?: string | null, display_price?: RadrootsCoreMoney | null, display_price_unit?: RadrootsCoreUnit | null, };
+export type RadrootsMessageFileDimensions = { w: number, h: number, };
-export type RadrootsListingDeliveryMethod = { kind: "pickup", } | { kind: "local_delivery", } | { kind: "shipping", } | { kind: "other", amount: { method: string, }, };
+export type RadrootsMessageRecipient = { public_key: string, relay_url?: string | null, };
-export type RadrootsListingImage = { url: string, size?: RadrootsListingImageSize | null, };
+export type RadrootsNip01EventWireDto = { id: string, pubkey: string, created_at: number, kind: number, tags: Array<Array<string>>, content: string, sig: string, extra: { [key: string]: unknown }, };
-export type RadrootsListingImageSize = { w: number, h: number, };
+export type RadrootsOperationalListing = { d_tag: string, published_at?: string | null, farm?: RadrootsFarmRef, product: RadrootsOperationalListingProduct, primary_bin_id: string, bins: Array<RadrootsOperationalListingBin>, resource_area?: RadrootsResourceAreaRef | null, plot?: RadrootsPlotRef | null, discounts?: Array<RadrootsCoreDiscount> | null, inventory_available?: string | null, availability?: RadrootsOperationalListingAvailability | null, delivery_method?: RadrootsOperationalListingDeliveryMethod | null, location?: RadrootsOperationalListingPublicLocation | null, images?: Array<RadrootsOperationalListingImage> | null, };
-export type RadrootsListingParseError = { InvalidKind: number, } | { MissingTag: string, } | { InvalidTag: string, } | { InvalidNumber: string, } | "InvalidUnit" | "InvalidCurrency" | { InvalidJson: string, } | { InvalidDiscount: string, };
+export type RadrootsOperationalListingAvailability = { kind: "window", amount: { start?: string | null, end?: string | null, }, } | { kind: "status", amount: { status: RadrootsOperationalListingStatus, }, };
-export type RadrootsListingProduct = { key: string, title: string, category: string, summary?: string | null, process?: string | null, lot?: string | null, location?: string | null, profile?: string | null, year?: string | null, };
+export type RadrootsOperationalListingBin = { bin_id: string, quantity: RadrootsCoreQuantity, price_per_canonical_unit: RadrootsCoreQuantityPrice, display_amount?: string | null, display_unit?: RadrootsCoreUnit | null, display_label?: string | null, display_price?: RadrootsCoreMoney | null, display_price_unit?: RadrootsCoreUnit | null, };
-export type RadrootsListingProductTagKeys = readonly ["key", "title", "category", "summary", "process", "lot", "location", "profile", "year"];
+export type RadrootsOperationalListingDeliveryMethod = { kind: "pickup", } | { kind: "local_delivery", } | { kind: "shipping", } | { kind: "other", amount: { method: string, }, };
-export type RadrootsListingPublicLocation = { primary: string, city?: string | null, region?: string | null, country?: string | null, geohash: string, };
+export type RadrootsOperationalListingImage = { url: string, size?: RadrootsOperationalListingImageSize | null, };
-export type RadrootsListingStatus = { kind: "active", } | { kind: "sold", } | { kind: "other", amount: { value: string, }, };
+export type RadrootsOperationalListingImageSize = { w: number, h: number, };
-export type RadrootsMessage = { recipients: Array<RadrootsMessageRecipient>, content: string, reply_to?: RadrootsEventPtr | null, subject?: string | null, };
+export type RadrootsOperationalListingParseError = { InvalidKind: number, } | { MissingTag: string, } | { InvalidTag: string, } | { InvalidNumber: string, } | "InvalidUnit" | "InvalidCurrency" | { InvalidJson: string, } | { InvalidDiscount: string, };
-export type RadrootsMessageFile = { recipients: Array<RadrootsMessageRecipient>, file_url: string, reply_to?: RadrootsEventPtr | null, subject?: string | null, file_type: string, encryption_algorithm: string, decryption_key: string, decryption_nonce: string, encrypted_hash: string, original_hash?: string | null, size?: string | null, dimensions?: RadrootsMessageFileDimensions | null, blurhash?: string | null, thumb?: string | null, fallbacks: Array<string>, };
+export type RadrootsOperationalListingProduct = { key: string, title: string, category: string, summary?: string | null, process?: string | null, lot?: string | null, location?: string | null, profile?: string | null, year?: string | null, };
-export type RadrootsMessageFileDimensions = { w: number, h: number, };
+export type RadrootsOperationalListingProductTagKeys = readonly ["key", "title", "category", "summary", "process", "lot", "location", "profile", "year"];
-export type RadrootsMessageRecipient = { public_key: string, relay_url?: string | null, };
+export type RadrootsOperationalListingPublicLocation = { primary: string, city?: string | null, region?: string | null, country?: string | null, geohash: string, };
-export type RadrootsNip01EventWireDto = { id: string, pubkey: string, created_at: number, kind: number, tags: Array<Array<string>>, content: string, sig: string, extra: { [key: string]: unknown }, };
+export type RadrootsOperationalListingStatus = { kind: "active", } | { kind: "sold", } | { kind: "other", amount: { value: string, }, };
+
+export type RadrootsOperationalListingValidationError = { kind: "invalid_kind", amount: { kind: number, }, } | { kind: "invalid_profile", } | { 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: RadrootsOperationalListingParseError, }, } | { 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 RadrootsPlot = { d_tag: string, farm: RadrootsFarmRef, name: string, about?: string | null, location?: RadrootsPlotLocation | null, tags?: Array<string> | null, };
@@ -184,8 +184,6 @@ export type RadrootsSocialMediaThumbnail = { url: string, dimensions?: RadrootsS
export type RadrootsSocialTarget = { kind: "event", id: string, author?: string | null, event_kind?: number | null, relays?: Array<string> | null, } | { kind: "address", address: string, author?: string | null, event_kind?: number | null, relays?: Array<string> | null, } | { kind: "external", id: string, external_kind: string, hint?: string | null, };
-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 RadrootsVerifiedSignedEventDto = { state: RadrootsVerifiedSignedEventVerificationStateDto, signed_event: RadrootsSignedEventDto, };
export type RadrootsVerifiedSignedEventVerificationStateDto = "signature_verified";
diff --git a/packages/trade-bindings/src/generated/types.ts b/packages/trade-bindings/src/generated/types.ts
@@ -1,7 +1,13 @@
// @generated by cargo xtask generate ts
// Do not edit by hand.
import type { RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreDiscount, RadrootsCoreMoney, RadrootsCoreQuantity, RadrootsCoreUnit } from "@radroots/core-bindings";
-import type { RadrootsFarmRef, RadrootsListing, RadrootsListingAvailability, RadrootsListingBin, RadrootsListingDeliveryMethod, RadrootsListingImage, RadrootsListingProduct, RadrootsListingPublicLocation, RadrootsPlotRef, RadrootsResourceAreaRef } from "@radroots/event-bindings";
+import type { RadrootsFarmRef, RadrootsOperationalListing, RadrootsOperationalListingAvailability, RadrootsOperationalListingBin, RadrootsOperationalListingDeliveryMethod, RadrootsOperationalListingImage, RadrootsOperationalListingProduct, RadrootsOperationalListingPublicLocation, RadrootsPlotRef, RadrootsResourceAreaRef } from "@radroots/event-bindings";
+
+export type RadrootsOperationalListingSubtotal = { price_amount: RadrootsCoreMoney, price_currency: RadrootsCoreCurrency, quantity_amount: RadrootsCoreDecimal, quantity_unit: RadrootsCoreUnit, };
+
+export type RadrootsOperationalListingTotal = { price_amount: RadrootsCoreMoney, price_currency: RadrootsCoreCurrency, quantity_amount: RadrootsCoreDecimal, quantity_unit: RadrootsCoreUnit, };
+
+export type RadrootsOperationalListingTradeProjection = { listing_id: string, listing_addr: string, seller_pubkey: string, title: string, description: string, product_type: string, primary_bin_id: string, bin_quantity: RadrootsCoreQuantity, unit: RadrootsCoreUnit, unit_price: RadrootsCoreMoney, inventory_available: RadrootsCoreDecimal, availability: RadrootsOperationalListingAvailability, location: RadrootsOperationalListingPublicLocation, delivery_method: RadrootsOperationalListingDeliveryMethod, listing: RadrootsOperationalListing, };
export type RadrootsTradeAgreementClaimV1 = { claim_mutation_id: string, proposal_mutation_id: string, candidate_id: string, candidate_author_pubkey: string, accepted_by_pubkey: string, reservation_commitment: string, };
@@ -21,21 +27,19 @@ export type RadrootsTradeFacetCount = { key: string, count: number, };
export type RadrootsTradeFulfillmentStateV1 = "not_started";
-export type RadrootsTradeListing = { listing_id: string, listing_addr: string, seller_pubkey: string, title: string, description: string, product_type: string, primary_bin_id: string, bin_quantity: RadrootsCoreQuantity, unit: RadrootsCoreUnit, unit_price: RadrootsCoreMoney, inventory_available: RadrootsCoreDecimal, availability: RadrootsListingAvailability, location: RadrootsListingPublicLocation, delivery_method: RadrootsListingDeliveryMethod, listing: RadrootsListing, };
-
export type RadrootsTradeListingBackofficeOverlay = { listing_addr: string, review_queue?: RadrootsTradeReviewQueueEntry | null, moderation_flags: Array<RadrootsTradeModerationFlag>, };
export type RadrootsTradeListingBackofficeQuery = { listing: RadrootsTradeListingQuery, requires_review?: boolean | null, has_open_moderation_flags?: boolean | null, };
export type RadrootsTradeListingBackofficeView = { listing: RadrootsTradeListingProjection, marketplace?: RadrootsTradeMarketplaceListingSummary | null, overlay?: RadrootsTradeListingBackofficeOverlay | null, requires_review: boolean, open_moderation_flag_count: number, };
-export type RadrootsTradeListingBinProjection = { bin: RadrootsListingBin, one_bin_total: RadrootsTradeListingTotal, };
+export type RadrootsTradeListingBinProjection = { bin: RadrootsOperationalListingBin, one_bin_total: RadrootsOperationalListingTotal, };
export type RadrootsTradeListingFacets = { seller_pubkeys: Array<RadrootsTradeFacetCount>, farm_pubkeys: Array<RadrootsTradeFacetCount>, farm_ids: Array<RadrootsTradeFacetCount>, product_keys: Array<RadrootsTradeFacetCount>, product_categories: Array<RadrootsTradeFacetCount>, listing_statuses: Array<RadrootsTradeFacetCount>, };
export type RadrootsTradeListingMarketStatus = "unknown" | "window" | "active" | "sold" | { other: { value: string, }, };
-export type RadrootsTradeListingProjection = { listing_addr: string, seller_pubkey: string, listing_id: string, farm: RadrootsFarmRef, product: RadrootsListingProduct, primary_bin_id: string, bins: Array<RadrootsTradeListingBinProjection>, resource_area?: RadrootsResourceAreaRef | null, plot?: RadrootsPlotRef | null, discounts?: Array<RadrootsCoreDiscount> | null, inventory_available?: RadrootsCoreDecimal | null, availability?: RadrootsListingAvailability | null, delivery_method?: RadrootsListingDeliveryMethod | null, location?: RadrootsListingPublicLocation | null, images?: Array<RadrootsListingImage> | null, trade_count: number, open_trade_count: number, terminal_trade_count: number, };
+export type RadrootsTradeListingProjection = { listing_addr: string, seller_pubkey: string, listing_id: string, farm: RadrootsFarmRef, product: RadrootsOperationalListingProduct, primary_bin_id: string, bins: Array<RadrootsTradeListingBinProjection>, resource_area?: RadrootsResourceAreaRef | null, plot?: RadrootsPlotRef | null, discounts?: Array<RadrootsCoreDiscount> | null, inventory_available?: RadrootsCoreDecimal | null, availability?: RadrootsOperationalListingAvailability | null, delivery_method?: RadrootsOperationalListingDeliveryMethod | null, location?: RadrootsOperationalListingPublicLocation | null, images?: Array<RadrootsOperationalListingImage> | null, trade_count: number, open_trade_count: number, terminal_trade_count: number, };
export type RadrootsTradeListingQuery = { seller_pubkey?: string | null, farm_pubkey?: string | null, farm_id?: string | null, product_key?: string | null, product_category?: string | null, listing_status?: RadrootsTradeListingMarketStatus | null, };
@@ -43,11 +47,7 @@ export type RadrootsTradeListingSort = { field: RadrootsTradeListingSortField, d
export type RadrootsTradeListingSortField = "listing_addr" | "product_title" | "product_category" | "seller_pubkey" | "inventory_available" | "open_trade_count" | "total_trade_count";
-export type RadrootsTradeListingSubtotal = { price_amount: RadrootsCoreMoney, price_currency: RadrootsCoreCurrency, quantity_amount: RadrootsCoreDecimal, quantity_unit: RadrootsCoreUnit, };
-
-export type RadrootsTradeListingTotal = { price_amount: RadrootsCoreMoney, price_currency: RadrootsCoreCurrency, quantity_amount: RadrootsCoreDecimal, quantity_unit: RadrootsCoreUnit, };
-
-export type RadrootsTradeMarketplaceListingSummary = { listing_addr: string, seller_pubkey: string, farm_pubkey: string, farm_id: string, product_key: string, product_title: string, product_category: string, product_summary?: string | null, listing_status: RadrootsTradeListingMarketStatus, location_primary?: string | null, inventory_available?: RadrootsCoreDecimal | null, primary_bin_id: string, primary_bin_label?: string | null, primary_bin_total: RadrootsTradeListingTotal, trade_count: number, open_trade_count: number, terminal_trade_count: number, };
+export type RadrootsTradeMarketplaceListingSummary = { listing_addr: string, seller_pubkey: string, farm_pubkey: string, farm_id: string, product_key: string, product_title: string, product_category: string, product_summary?: string | null, listing_status: RadrootsTradeListingMarketStatus, location_primary?: string | null, inventory_available?: RadrootsCoreDecimal | null, primary_bin_id: string, primary_bin_label?: string | null, primary_bin_total: RadrootsOperationalListingTotal, trade_count: number, open_trade_count: number, terminal_trade_count: number, };
export type RadrootsTradeModerationFlag = { code: string, severity: RadrootsTradeModerationSeverity, status: RadrootsTradeModerationStatus, source?: string | null, reason?: string | null, };
diff --git a/tools/xtask/src/dto_roots.rs b/tools/xtask/src/dto_roots.rs
@@ -134,14 +134,14 @@ const TRADE_EXTERNAL_OVERRIDES: &[DtoExternalOverride] = &[
core_override("RadrootsCoreQuantityPrice"),
core_override("RadrootsCoreUnit"),
event_override("RadrootsFarmRef"),
- event_override("RadrootsListing"),
- event_override("RadrootsListingAvailability"),
- event_override("RadrootsListingBin"),
- event_override("RadrootsListingDeliveryMethod"),
- event_override("RadrootsListingImage"),
- event_override("RadrootsListingProduct"),
- event_override("RadrootsListingPublicLocation"),
- event_override("RadrootsListingStatus"),
+ event_override("RadrootsOperationalListing"),
+ event_override("RadrootsOperationalListingAvailability"),
+ event_override("RadrootsOperationalListingBin"),
+ event_override("RadrootsOperationalListingDeliveryMethod"),
+ event_override("RadrootsOperationalListingImage"),
+ event_override("RadrootsOperationalListingProduct"),
+ event_override("RadrootsOperationalListingPublicLocation"),
+ event_override("RadrootsOperationalListingStatus"),
event_override("RadrootsEventPtr"),
event_override("RadrootsPlotRef"),
event_override("RadrootsResourceAreaRef"),
@@ -451,7 +451,7 @@ fn with_event_sdk_wrappers(module: DtoTypesModule) -> DtoTypesModule {
.map(str::to_owned)
.collect::<Vec<_>>();
declarations.push(type_alias(
- "RadrootsListingProductTagKeys",
+ "RadrootsOperationalListingProductTagKeys",
TypeScriptType::readonly_tuple(
[
"key", "title", "category", "summary", "process", "lot", "location", "profile",
@@ -583,7 +583,6 @@ mod tests {
"RadrootsAccountClaim",
"RadrootsAddressableRef",
"RadrootsAppData",
- "RadrootsComment",
"RadrootsCommercialDomain",
"RadrootsContributionAttestation",
"RadrootsCoop",
@@ -630,22 +629,23 @@ mod tests {
"RadrootsList",
"RadrootsListEntry",
"RadrootsListSet",
- "RadrootsListing",
- "RadrootsListingAvailability",
- "RadrootsListingBin",
- "RadrootsListingDeliveryMethod",
- "RadrootsListingImage",
- "RadrootsListingImageSize",
- "RadrootsListingParseError",
- "RadrootsListingProduct",
- "RadrootsListingProductTagKeys",
- "RadrootsListingPublicLocation",
- "RadrootsListingStatus",
"RadrootsMessage",
"RadrootsMessageFile",
"RadrootsMessageFileDimensions",
"RadrootsMessageRecipient",
"RadrootsNip01EventWireDto",
+ "RadrootsOperationalListing",
+ "RadrootsOperationalListingAvailability",
+ "RadrootsOperationalListingBin",
+ "RadrootsOperationalListingDeliveryMethod",
+ "RadrootsOperationalListingImage",
+ "RadrootsOperationalListingImageSize",
+ "RadrootsOperationalListingParseError",
+ "RadrootsOperationalListingProduct",
+ "RadrootsOperationalListingProductTagKeys",
+ "RadrootsOperationalListingPublicLocation",
+ "RadrootsOperationalListingStatus",
+ "RadrootsOperationalListingValidationError",
"RadrootsPlot",
"RadrootsPlotLocation",
"RadrootsPlotRef",
@@ -668,7 +668,6 @@ mod tests {
"RadrootsSocialMediaMetadata",
"RadrootsSocialMediaThumbnail",
"RadrootsSocialTarget",
- "RadrootsTradeValidationListingError",
"RadrootsVerifiedSignedEventDto",
"RadrootsVerifiedSignedEventVerificationStateDto",
"RadrootsWikiArticle",
@@ -685,6 +684,9 @@ mod tests {
"RadrootsEventIndexCheckpoint",
];
const TRADE_TYPE_INVENTORY: &[&str] = &[
+ "RadrootsOperationalListingSubtotal",
+ "RadrootsOperationalListingTotal",
+ "RadrootsOperationalListingTradeProjection",
"RadrootsTradeAgreementClaimV1",
"RadrootsTradeAgreementStateV1",
"RadrootsTradeAttestationRecordV1",
@@ -694,7 +696,6 @@ mod tests {
"RadrootsTradeEvidenceStateV1",
"RadrootsTradeFacetCount",
"RadrootsTradeFulfillmentStateV1",
- "RadrootsTradeListing",
"RadrootsTradeListingBackofficeOverlay",
"RadrootsTradeListingBackofficeQuery",
"RadrootsTradeListingBackofficeView",
@@ -705,8 +706,6 @@ mod tests {
"RadrootsTradeListingQuery",
"RadrootsTradeListingSort",
"RadrootsTradeListingSortField",
- "RadrootsTradeListingSubtotal",
- "RadrootsTradeListingTotal",
"RadrootsTradeMarketplaceListingSummary",
"RadrootsTradeModerationFlag",
"RadrootsTradeModerationSeverity",
@@ -1004,16 +1003,16 @@ mod tests {
assert!(
imports
.get("@radroots/event-bindings")
- .is_some_and(|names| names.contains("RadrootsListing"))
+ .is_some_and(|names| names.contains("RadrootsOperationalListing"))
);
assert!(
imports
.get("@radroots/event-bindings")
- .is_some_and(|names| names.contains("RadrootsListingBin"))
+ .is_some_and(|names| names.contains("RadrootsOperationalListingBin"))
);
for duplicate in [
- "export type RadrootsListing = ",
+ "export type RadrootsOperationalListing = ",
"export type RadrootsFarmRef = ",
"export type RadrootsOrderEconomics = ",
"export type RadrootsOrderInventoryCommitment = ",
diff --git a/tools/xtask/src/smoke.rs b/tools/xtask/src/smoke.rs
@@ -123,14 +123,14 @@ const CONSUMER_MAIN: &str = r#"use nostr::{EventBuilder, Keys, Kind, Tag, Timest
use radroots_event::RadrootsEventEnvelopeParts;
use radroots_sdk::knowledge::prelude::*;
-const SECRET_KEY_HEX: &str = "0101010101010101010101010101010101010101010101010101010101010101";
const CREATED_AT: u32 = 1_800_000_000;
fn main() -> Result<(), Box<dyn std::error::Error>> {
+ let keys = Keys::generate();
let claim = claim_builder().build()?;
let parts = claim_builder().build_event()?;
- let draft = claim_builder().build_draft(public_key_hex(), CREATED_AT)?;
- let decoded = verify_and_decode_radroots_event(sign_parts(parts)?)?;
+ let draft = claim_builder().build_draft(keys.public_key().to_hex(), CREATED_AT)?;
+ let decoded = verify_and_decode_radroots_event(sign_parts(parts, &keys)?)?;
let manifest = contract_manifest();
assert_eq!(draft.contract_id, KNOWLEDGE_CLAIM_CONTRACT_ID);
@@ -142,17 +142,19 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
Ok(())
}
-fn sign_parts(parts: RadrootsNip01EventWireParts) -> Result<RadrootsEventEnvelope, Box<dyn std::error::Error>> {
+fn sign_parts(
+ parts: RadrootsNip01EventWireParts,
+ keys: &Keys,
+) -> Result<RadrootsEventEnvelope, Box<dyn std::error::Error>> {
let tags = parts
.tags
.into_iter()
.map(Tag::parse)
.collect::<Result<Vec<_>, _>>()?;
- let keys = Keys::parse(SECRET_KEY_HEX)?;
let event = EventBuilder::new(Kind::Custom(parts.kind as u16), parts.content)
.tags(tags)
.custom_created_at(Timestamp::from_secs(u64::from(CREATED_AT)))
- .sign_with_keys(&keys)?;
+ .sign_with_keys(keys)?;
Ok(RadrootsEventEnvelope::new(RadrootsEventEnvelopeParts {
id: event.id.to_hex(),
author: event.pubkey.to_hex(),
@@ -169,13 +171,6 @@ fn sign_parts(parts: RadrootsNip01EventWireParts) -> Result<RadrootsEventEnvelop
})?)
}
-fn public_key_hex() -> String {
- Keys::parse(SECRET_KEY_HEX)
- .expect("keys")
- .public_key()
- .to_hex()
-}
-
fn hex_64(character: char) -> String {
character.to_string().repeat(64)
}
diff --git a/tools/xtask/src/wasm_declarations.rs b/tools/xtask/src/wasm_declarations.rs
@@ -355,7 +355,6 @@ fn promise_of(type_expr: TypeScriptType) -> TypeScriptType {
const EVENT_CODEC_FUNCTIONS: &[WasmPublicFunction] = &[
public_function!("article_tags", Some("article_json"), String, 2, 4),
- public_function!("comment_tags", Some("comment_json"), String, 2, 4),
public_function!("coop_tags", Some("coop_json"), String, 2, 4),
public_function!("document_tags", Some("document_json"), String, 2, 4),
public_function!("farm_crdt_change_tags", Some("input_json"), String, 2, 4),
@@ -408,17 +407,56 @@ const EVENT_CODEC_FUNCTIONS: &[WasmPublicFunction] = &[
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),
- public_function!("listing_tags_full", Some("listing_json"), String, 2, 4),
+ public_function!(
+ "operational_listing_tags",
+ Some("listing_json"),
+ String,
+ 2,
+ 4
+ ),
+ public_function!(
+ "operational_listing_tags_full",
+ Some("listing_json"),
+ String,
+ 2,
+ 4
+ ),
public_function!("message_file_tags", Some("message_json"), String, 2, 4),
public_function!("message_tags", Some("message_json"), String, 2, 4),
public_function!("plot_tags", Some("plot_json"), String, 2, 4),
- public_function!("post_tags", Some("post_json"), String, 2, 4),
public_function!("reaction_tags", Some("reaction_json"), String, 2, 4),
public_function!("relay_auth_tags", Some("auth_json"), String, 2, 4),
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!(
+ "social_ask_build_authored_draft",
+ Some("input_json"),
+ String,
+ 2,
+ 4
+ ),
+ public_function!(
+ "social_comment_build_authored_draft",
+ Some("input_json"),
+ String,
+ 2,
+ 4
+ ),
+ public_function!(
+ "social_photo_update_build_authored_draft",
+ Some("input_json"),
+ String,
+ 2,
+ 4
+ ),
+ public_function!(
+ "social_update_build_authored_draft",
+ Some("input_json"),
+ String,
+ 2,
+ 4
+ ),
public_function!("wiki_article_tags", Some("article_json"), String, 2, 4),
public_function!(
"wiki_merge_request_tags",
@@ -1013,12 +1051,33 @@ mod tests {
fn generated_declarations_include_public_wasm_entrypoints() {
let specs = wasm_package_specs();
let events = declaration_files(specs[0]).expect("event declarations");
- assert!(events[0].contents.contains("export function listing_tags"));
+ assert!(
+ events[0]
+ .contents
+ .contains("export function operational_listing_tags")
+ );
assert!(
events[1]
.contents
- .contains("export declare const listing_tags")
+ .contains("export declare const operational_listing_tags")
);
+ for authored in [
+ "social_ask_build_authored_draft",
+ "social_comment_build_authored_draft",
+ "social_photo_update_build_authored_draft",
+ "social_update_build_authored_draft",
+ ] {
+ assert!(
+ events[0]
+ .contents
+ .contains(&format!("export function {authored}"))
+ );
+ assert!(
+ events[1]
+ .contents
+ .contains(&format!("export declare const {authored}"))
+ );
+ }
for retired in [
"calendar_date_event_tags",
"calendar_event_rsvp_tags",
@@ -1028,6 +1087,18 @@ mod tests {
assert!(!events[0].contents.contains(retired));
assert!(!events[1].contents.contains(retired));
}
+ for retired in ["comment_tags", "post_tags"] {
+ assert!(
+ !events[0]
+ .contents
+ .contains(&format!("export function {retired}("))
+ );
+ assert!(
+ !events[1]
+ .contents
+ .contains(&format!("export declare const {retired}:"))
+ );
+ }
let replica_store = declaration_files(specs[1]).expect("replica store declarations");
assert!(