commit 862c78d5d63b2282dac0e8eb75b8971e5b11d585
parent 27c621961ecfd7fcd0c1aa3513d285596378ccd6
Author: triesap <tyson@radroots.org>
Date: Mon, 3 Aug 2026 11:23:41 +0000
sdk: refactor farm prepare and enqueue operations
- freeze pure farm plans from canonical event types
- retain private location authority in private storage
- enqueue through native sync commit and idempotency law
- verify purity cancellation replay and delivery policy
Diffstat:
7 files changed, 547 insertions(+), 3471 deletions(-)
diff --git a/crates/sdk/Cargo.toml b/crates/sdk/Cargo.toml
@@ -56,7 +56,10 @@ full = [
[dependencies]
radroots_core = { workspace = true, default-features = false }
radroots_event = { workspace = true, default-features = false }
-radroots_event_codec = { workspace = true, default-features = false }
+radroots_event_codec = { workspace = true, default-features = false, features = [
+ "json",
+ "std",
+] }
radroots_identity = { workspace = true, default-features = false }
radroots_protocol = { workspace = true, default-features = false }
radroots_signing = { workspace = true, default-features = false }
diff --git a/crates/sdk/src/client.rs b/crates/sdk/src/client.rs
@@ -239,6 +239,12 @@ impl Client {
Ok(self.inner.sync.as_ref().map(crate::sync::Operations::new))
}
+ /// Returns farm commit operations when canonical synchronization is configured.
+ #[cfg(feature = "sync")]
+ pub fn farm(&self) -> Result<Option<crate::farm::Operations<'_>>> {
+ Ok(self.sync()?.map(crate::farm::Operations::new))
+ }
+
/// Returns whether explicit close completed successfully or reached the
/// lower storage commit point.
#[must_use]
diff --git a/crates/sdk/src/farm.rs b/crates/sdk/src/farm.rs
@@ -1 +1,502 @@
-//! Farm product operations.
+//! Side-effect-free farm planning and canonical durable enqueue operations.
+
+use std::{error, fmt};
+
+use radroots_event::{
+ EventDraft,
+ contract::AuthorRole,
+ envelope::kind::KIND_FARM,
+ farm::Farm,
+ id::{AddressableCoordinate, ParseError},
+};
+use radroots_event_codec::{encode::EventEncodeError, encode::farm::to_wire_parts};
+use radroots_signing::Actor;
+
+const FARM_PROFILE_CONTRACT_ID: &str = "radroots.farm.profile.v1";
+
+/// Pure inputs for one frozen farm profile plan.
+#[derive(Clone, Debug)]
+pub struct PrepareRequest {
+ actor: Actor,
+ farm: Farm,
+ created_at_unix: u64,
+}
+
+impl PrepareRequest {
+ /// Creates explicit canonical planning inputs.
+ #[must_use]
+ pub const fn new(actor: Actor, farm: Farm, created_at_unix: u64) -> Self {
+ Self {
+ actor,
+ farm,
+ created_at_unix,
+ }
+ }
+}
+
+/// Frozen, replay-stable farm publication plan.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct Plan {
+ actor: Actor,
+ coordinate: AddressableCoordinate,
+ draft: EventDraft,
+}
+
+impl Plan {
+ /// Returns the exact authorized actor carried into signing.
+ #[must_use]
+ pub const fn actor(&self) -> &Actor {
+ &self.actor
+ }
+
+ /// Returns the canonical addressable farm coordinate.
+ #[must_use]
+ pub const fn coordinate(&self) -> &AddressableCoordinate {
+ &self.coordinate
+ }
+
+ /// Returns the frozen canonical event draft.
+ #[must_use]
+ pub const fn draft(&self) -> &EventDraft {
+ &self.draft
+ }
+}
+
+/// Farm plan validation stage.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+#[non_exhaustive]
+pub enum PrepareErrorKind {
+ /// The actor does not claim the required farm author role.
+ UnauthorizedActor,
+ /// The canonical farm codec rejected the native farm model.
+ Encode,
+ /// The canonical addressable coordinate rejected the farm identity.
+ Coordinate,
+ /// The canonical event draft rejected the encoded parts.
+ Draft,
+}
+
+/// One secret-safe farm planning failure retaining its lower source.
+pub struct PrepareError {
+ kind: PrepareErrorKind,
+ source: Option<Box<dyn error::Error + Send + Sync>>,
+}
+
+impl PrepareError {
+ /// Returns the stable client-level planning stage.
+ #[must_use]
+ pub const fn kind(&self) -> PrepareErrorKind {
+ self.kind
+ }
+
+ fn unauthorized_actor() -> Self {
+ Self {
+ kind: PrepareErrorKind::UnauthorizedActor,
+ source: None,
+ }
+ }
+
+ fn encode(source: EventEncodeError) -> Self {
+ Self::with_source(PrepareErrorKind::Encode, source)
+ }
+
+ fn coordinate(source: ParseError) -> Self {
+ Self::with_source(PrepareErrorKind::Coordinate, source)
+ }
+
+ fn draft(source: radroots_event::draft::DraftError) -> Self {
+ Self::with_source(PrepareErrorKind::Draft, source)
+ }
+
+ fn with_source(
+ kind: PrepareErrorKind,
+ source: impl error::Error + Send + Sync + 'static,
+ ) -> Self {
+ Self {
+ kind,
+ source: Some(Box::new(source)),
+ }
+ }
+}
+
+impl fmt::Display for PrepareError {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str(match self.kind {
+ PrepareErrorKind::UnauthorizedActor => "farm actor is not authorized",
+ PrepareErrorKind::Encode => "farm model is invalid",
+ PrepareErrorKind::Coordinate => "farm coordinate is invalid",
+ PrepareErrorKind::Draft => "farm event draft is invalid",
+ })
+ }
+}
+
+impl fmt::Debug for PrepareError {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter
+ .debug_struct("PrepareError")
+ .field("kind", &self.kind)
+ .finish_non_exhaustive()
+ }
+}
+
+impl error::Error for PrepareError {
+ fn source(&self) -> Option<&(dyn error::Error + 'static)> {
+ self.source
+ .as_deref()
+ .map(|source| source as &(dyn error::Error + 'static))
+ }
+}
+
+/// Validates and freezes one farm profile without storage, signing, or network work.
+///
+/// `Farm` contains only public profile locality. Exact coordinates and other
+/// private farm artifacts are deliberately not accepted here; hosts persist
+/// their typed references and metadata through
+/// `radroots_storage::private_artifact::PrivateArtifactStore`.
+pub fn prepare(request: PrepareRequest) -> Result<Plan, PrepareError> {
+ if !request.actor.satisfies(AuthorRole::Farmer) {
+ return Err(PrepareError::unauthorized_actor());
+ }
+ let parts = to_wire_parts(&request.farm).map_err(PrepareError::encode)?;
+ let coordinate = AddressableCoordinate::parse(format!(
+ "{KIND_FARM}:{}:{}",
+ request.actor.public_key(),
+ request.farm.d_tag
+ ))
+ .map_err(PrepareError::coordinate)?;
+ let draft = EventDraft::new(
+ FARM_PROFILE_CONTRACT_ID,
+ parts.kind,
+ request.created_at_unix,
+ parts.tags,
+ parts.content,
+ request.actor.public_key().to_hex(),
+ )
+ .map_err(PrepareError::draft)?;
+ Ok(Plan {
+ actor: request.actor,
+ coordinate,
+ draft,
+ })
+}
+
+#[cfg(feature = "sync")]
+use radroots_signing::request::CancellationPolicy;
+#[cfg(feature = "sync")]
+use radroots_storage::journal::IdempotencyKey;
+#[cfg(feature = "sync")]
+use radroots_sync::{
+ PushReceipt,
+ policy::{Error as SyncError, SyncId},
+};
+
+/// Explicit commit inputs for one prepared farm publication.
+#[cfg(feature = "sync")]
+#[derive(Clone, Debug)]
+pub struct EnqueueRequest {
+ operation_id: SyncId,
+ idempotency_key: IdempotencyKey,
+ plan: Plan,
+ profile: crate::transport::Profile,
+ cancellation: CancellationPolicy,
+}
+
+#[cfg(feature = "sync")]
+impl EnqueueRequest {
+ /// Creates an enqueue request whose transport selection has no fallback.
+ #[must_use]
+ pub const fn new(
+ operation_id: SyncId,
+ idempotency_key: IdempotencyKey,
+ plan: Plan,
+ profile: crate::transport::Profile,
+ cancellation: CancellationPolicy,
+ ) -> Self {
+ Self {
+ operation_id,
+ idempotency_key,
+ plan,
+ profile,
+ cancellation,
+ }
+ }
+}
+
+/// Borrowed farm commit operations over the canonical sync engine.
+#[cfg(feature = "sync")]
+#[derive(Clone, Copy, Debug)]
+pub struct Operations<'a> {
+ sync: crate::sync::Operations<'a>,
+}
+
+#[cfg(feature = "sync")]
+impl<'a> Operations<'a> {
+ pub(crate) const fn new(sync: crate::sync::Operations<'a>) -> Self {
+ Self { sync }
+ }
+
+ /// Signs and atomically enqueues a prepared farm publication.
+ ///
+ /// Before the lower atomic enqueue commit, cancellation may leave only
+ /// recoverable prepared/signed journal state. After commit, cancellation
+ /// cannot claim rollback; replay with the same idempotency input returns
+ /// the durable outbox record.
+ pub async fn enqueue(&self, request: EnqueueRequest) -> Result<PushReceipt, SyncError> {
+ let targets = request
+ .profile
+ .targets()
+ .cloned()
+ .ok_or(SyncError::InvalidPushRequest)?;
+ let satisfaction = request
+ .profile
+ .satisfaction()
+ .cloned()
+ .ok_or(SyncError::InvalidPushRequest)?;
+ self.sync
+ .sign_and_enqueue(radroots_sync::PushRequest::new(
+ request.operation_id,
+ request.idempotency_key,
+ request.plan.actor,
+ request.plan.draft,
+ targets,
+ satisfaction,
+ request.cancellation,
+ )?)
+ .await
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use radroots_event::farm::FarmPublicLocation;
+ use radroots_signing::actor::ActorSource;
+
+ use super::*;
+
+ const PUBLIC_KEY: &str = "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df";
+
+ fn actor(role: AuthorRole) -> Actor {
+ Actor::from_public_key_hex(PUBLIC_KEY, ActorSource::ExplicitPublicKey, [role])
+ .expect("actor")
+ }
+
+ fn farm() -> Farm {
+ Farm {
+ d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_owned(),
+ name: "Moss Street Farm".to_owned(),
+ about: Some("seasonal vegetables".to_owned()),
+ website: None,
+ picture: None,
+ banner: None,
+ location: Some(FarmPublicLocation {
+ primary: "Santa Cruz, California".to_owned(),
+ city: Some("Santa Cruz".to_owned()),
+ region: Some("California".to_owned()),
+ country: Some("US".to_owned()),
+ geohash: "9q8yy".to_owned(),
+ }),
+ tags: Some(vec!["vegetables".to_owned()]),
+ }
+ }
+
+ #[test]
+ fn prepare_is_pure_deterministic_and_uses_canonical_public_types() {
+ let request = PrepareRequest::new(actor(AuthorRole::Farmer), farm(), 1_800_000_000);
+ let first = prepare(request.clone()).expect("first plan");
+ let second = prepare(request).expect("second plan");
+
+ assert_eq!(first, second);
+ assert_eq!(first.draft().contract_id(), FARM_PROFILE_CONTRACT_ID);
+ assert_eq!(first.draft().kind_u32(), KIND_FARM);
+ assert_eq!(first.draft().created_at_u64(), 1_800_000_000);
+ assert_eq!(
+ first.draft().expected_pubkey(),
+ &actor(AuthorRole::Farmer).public_key()
+ );
+ assert_eq!(
+ first.coordinate().as_str(),
+ format!("{KIND_FARM}:{PUBLIC_KEY}:AAAAAAAAAAAAAAAAAAAAAA")
+ );
+ assert!(first.draft().content().contains("Moss Street Farm"));
+ assert!(!first.draft().content().contains("latitude"));
+ assert!(!first.draft().content().contains("longitude"));
+ }
+
+ #[test]
+ fn prepare_maps_authorization_and_lower_validation_once() {
+ let unauthorized = prepare(PrepareRequest::new(
+ actor(AuthorRole::Buyer),
+ farm(),
+ 1_800_000_000,
+ ))
+ .expect_err("unauthorized");
+ assert_eq!(unauthorized.kind(), PrepareErrorKind::UnauthorizedActor);
+ assert!(std::error::Error::source(&unauthorized).is_none());
+
+ let mut invalid = farm();
+ invalid.name.clear();
+ let encoded = prepare(PrepareRequest::new(
+ actor(AuthorRole::Farmer),
+ invalid,
+ 1_800_000_000,
+ ))
+ .expect_err("invalid model");
+ assert_eq!(encoded.kind(), PrepareErrorKind::Encode);
+ assert!(std::error::Error::source(&encoded).is_some());
+ assert_eq!(encoded.to_string(), "farm model is invalid");
+ assert!(!format!("{encoded:?}").contains("name"));
+ }
+
+ #[cfg(all(feature = "sync", feature = "memory", feature = "local-signing"))]
+ mod enqueue {
+ use std::sync::{
+ Arc,
+ atomic::{AtomicU8, Ordering},
+ };
+
+ use radroots_storage::{
+ Outbox, event::SourceGeneration, journal::IdempotencyKey, memory::MemoryStorage,
+ };
+ use radroots_sync::{
+ Engine,
+ policy::{Clock, DeadlinePolicy, Error, IdSource, OperationKind, SyncId, SyncStorage},
+ };
+ use radroots_transport::{
+ DeliveryReceipt, DeliveryRequest, Error as TransportError, EventSink, SinkStatus,
+ Target, TargetSet, TransportId,
+ capability::{Availability, Maturity, SinkCapabilities},
+ policy::{SatisfactionClass, SatisfactionPolicy, TargetPolicy},
+ };
+
+ use crate::{ClientBuilder, transport::Profile};
+
+ use super::*;
+
+ struct FixedClock;
+ struct SequenceIds(AtomicU8);
+ struct NoopSink;
+
+ impl Clock for FixedClock {
+ fn now_unix_ms(&self) -> Result<u64, Error> {
+ Ok(2_000_000_000_000)
+ }
+ }
+
+ impl IdSource for SequenceIds {
+ fn next_id(&self, _operation: OperationKind) -> Result<SyncId, Error> {
+ SyncId::new([self.0.fetch_add(1, Ordering::Relaxed); 16])
+ }
+ }
+
+ impl EventSink for NoopSink {
+ fn status(
+ &self,
+ ) -> radroots_transport::BoxFuture<'_, Result<SinkStatus, TransportError>> {
+ Box::pin(async {
+ Ok(SinkStatus::new(
+ TransportId::NOSTR,
+ true,
+ Maturity::Stable,
+ Availability::Available,
+ SinkCapabilities::DELIVER,
+ "ready",
+ ))
+ })
+ }
+
+ fn deliver(
+ &self,
+ _request: DeliveryRequest,
+ ) -> radroots_transport::BoxFuture<'_, Result<DeliveryReceipt, TransportError>>
+ {
+ Box::pin(async { Err(TransportError::UnsupportedOperation) })
+ }
+ }
+
+ #[tokio::test]
+ async fn enqueue_preserves_commit_cancellation_idempotency_and_delivery_policy() {
+ let storage = Arc::new(MemoryStorage::new(
+ SourceGeneration::new([4; 32]).expect("generation"),
+ ));
+ let signer =
+ Arc::new(radroots_nostr::signing::LocalSigner::generate().expect("local signer"));
+ let farm_actor = Actor::new(
+ signer.public_key(),
+ ActorSource::ExplicitPublicKey,
+ [AuthorRole::Farmer],
+ )
+ .expect("actor");
+ let plan =
+ prepare(PrepareRequest::new(farm_actor, farm(), 1_800_000_000)).expect("plan");
+ let targets = TargetSet::new(vec![
+ Target::nostr_relay("wss://farm.example").expect("target"),
+ ])
+ .expect("targets");
+ let satisfaction =
+ SatisfactionPolicy::new(SatisfactionClass::Delivered, TargetPolicy::all());
+ let profile = Profile::delivery(targets.clone(), satisfaction.clone())
+ .expect("transport profile");
+ let capability: Arc<dyn SyncStorage> = storage.clone();
+ let engine = Engine::builder(
+ capability,
+ Arc::new(FixedClock),
+ Arc::new(SequenceIds(AtomicU8::new(1))),
+ DeadlinePolicy::new(1_000, 1_000, 1_000).expect("deadlines"),
+ )
+ .sink(Arc::new(NoopSink))
+ .signer(signer)
+ .build()
+ .expect("engine");
+ let client = ClientBuilder::new()
+ .storage(storage.clone())
+ .sync_engine(engine)
+ .build()
+ .expect("client");
+ let operations = client.farm().expect("open").expect("farm operations");
+ let request = EnqueueRequest::new(
+ SyncId::new([7; 16]).expect("operation id"),
+ IdempotencyKey::parse("farm-publish-a").expect("idempotency key"),
+ plan,
+ profile,
+ CancellationPolicy::PreservePublishedRequest,
+ );
+
+ drop(operations.enqueue(request.clone()));
+ assert_eq!(
+ Outbox::status(storage.as_ref())
+ .await
+ .expect("outbox status")
+ .pending,
+ 0
+ );
+
+ let committed = operations
+ .enqueue(request.clone())
+ .await
+ .expect("committed enqueue");
+ assert!(!committed.is_replay());
+ assert_eq!(committed.outbox().request().target_set(), &targets);
+ assert_eq!(committed.outbox().request().satisfaction(), &satisfaction);
+ let replay = operations.enqueue(request).await.expect("replay");
+ assert!(replay.is_replay());
+ assert_eq!(replay.outbox().item_id(), committed.outbox().item_id());
+
+ let unavailable = EnqueueRequest::new(
+ SyncId::new([8; 16]).expect("operation id"),
+ IdempotencyKey::parse("farm-publish-preview").expect("idempotency key"),
+ prepare(PrepareRequest::new(
+ actor(AuthorRole::Farmer),
+ farm(),
+ 1_800_000_000,
+ ))
+ .expect("plan"),
+ Profile::unavailable_preview(TransportId::RETICULUM),
+ CancellationPolicy::PreservePublishedRequest,
+ );
+ assert_eq!(
+ operations.enqueue(unavailable).await,
+ Err(Error::InvalidPushRequest)
+ );
+ }
+ }
+}
diff --git a/crates/sdk/src/farms_runtime.rs b/crates/sdk/src/farms_runtime.rs
@@ -1,1076 +0,0 @@
-#[cfg(feature = "signer-adapters")]
-use crate::workflow_runtime::enqueue_configured_signed_workflow;
-#[cfg(feature = "runtime")]
-use crate::{
- FarmsClient, NostrRelayUrlPolicy, RadrootsSdkError, RadrootsSdkTimestamp, SatisfactionPolicy,
- SdkIdempotencyKey, SdkMutationState, TargetPolicy, farm,
- geonames::{
- Geocoder, GeocoderLocalityCandidate, GeocoderLocalityLookup, GeocoderLocalityQuery,
- GeocoderPoint, GeocoderReverseOptions, GeocoderReverseResult,
- },
- private_store::SdkPrivateFarmLocationRecord,
- workflow_runtime::{SdkWorkflowEnqueueRequest, enqueue_signed_workflow},
-};
-#[cfg(feature = "runtime")]
-use radroots_event::{
- contract::AuthorRole,
- draft::EventDraft,
- envelope::kind::KIND_FARM,
- farm::{Farm, FarmPublicLocation},
- id::{AddressableCoordinate, EventId},
- listing::operational::OperationalListingPublicLocation,
-};
-#[cfg(feature = "runtime")]
-use radroots_signing::{Actor, Signer};
-#[cfg(feature = "runtime")]
-pub const FARM_PUBLISH_OPERATION_KIND: &str = "farm.publish.v1";
-
-#[cfg(feature = "runtime")]
-const FARM_PROFILE_CONTRACT_ID: &str = "radroots.farm.profile.v1";
-#[cfg(feature = "runtime")]
-const FARM_PRIVATE_LOCATION_OPERATION: &str = "farm.private_location.upsert";
-#[cfg(feature = "runtime")]
-const GEOHASH5_LEN: usize = 5;
-#[cfg(feature = "runtime")]
-const GEOHASH_BASE32: &[u8; 32] = b"0123456789bcdefghjkmnpqrstuvwxyz";
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, serde::Serialize)]
-#[non_exhaustive]
-pub struct FarmPreparePublishRequest {
- #[serde(serialize_with = "crate::actor_json::serialize_actor_context")]
- pub actor: Actor,
- pub farm: Farm,
- pub created_at: Option<RadrootsSdkTimestamp>,
-}
-
-#[cfg(feature = "runtime")]
-impl FarmPreparePublishRequest {
- pub fn new(actor: Actor, farm: Farm) -> Self {
- Self {
- actor,
- farm,
- created_at: None,
- }
- }
-
- pub fn with_created_at(mut self, created_at: RadrootsSdkTimestamp) -> Self {
- self.created_at = Some(created_at);
- self
- }
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, serde::Serialize)]
-#[non_exhaustive]
-pub struct FarmEnqueuePublishRequest {
- #[serde(serialize_with = "crate::actor_json::serialize_actor_context")]
- pub actor: Actor,
- pub farm: Farm,
- pub target_policy: TargetPolicy,
- pub idempotency_key: Option<SdkIdempotencyKey>,
- pub created_at: Option<RadrootsSdkTimestamp>,
-}
-
-#[cfg(feature = "runtime")]
-impl FarmEnqueuePublishRequest {
- pub fn new(actor: Actor, farm: Farm, target_policy: TargetPolicy) -> Self {
- Self {
- actor,
- farm,
- target_policy,
- idempotency_key: None,
- created_at: None,
- }
- }
-
- pub fn try_with_nostr_targets<I, S>(
- mut self,
- target_policy: I,
- policy: NostrRelayUrlPolicy,
- ) -> Result<Self, RadrootsSdkError>
- where
- I: IntoIterator<Item = S>,
- S: AsRef<str>,
- {
- self.target_policy = TargetPolicy::try_nostr_relays(target_policy, policy)?;
- Ok(self)
- }
-
- pub fn with_idempotency_key(mut self, idempotency_key: SdkIdempotencyKey) -> Self {
- self.idempotency_key = Some(idempotency_key);
- self
- }
-
- pub fn try_with_idempotency_key(
- mut self,
- idempotency_key: impl AsRef<str>,
- ) -> Result<Self, RadrootsSdkError> {
- self.idempotency_key = Some(SdkIdempotencyKey::new(idempotency_key)?);
- Ok(self)
- }
-
- pub fn with_created_at(mut self, created_at: RadrootsSdkTimestamp) -> Self {
- self.created_at = Some(created_at);
- self
- }
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-pub struct FarmPublishPlan {
- farm_addr: AddressableCoordinate,
- expected_event_id: EventId,
- frozen_draft: EventDraft,
- created_at: RadrootsSdkTimestamp,
-}
-
-#[cfg(feature = "runtime")]
-impl FarmPublishPlan {
- pub fn farm_addr(&self) -> &AddressableCoordinate {
- &self.farm_addr
- }
-
- pub fn expected_event_id(&self) -> &EventId {
- &self.expected_event_id
- }
-
- pub fn frozen_draft(&self) -> &EventDraft {
- &self.frozen_draft
- }
-
- pub fn created_at(&self) -> RadrootsSdkTimestamp {
- self.created_at
- }
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
-pub struct FarmEnqueueReceipt {
- pub farm_addr: AddressableCoordinate,
- pub expected_event_id: EventId,
- pub signed_event_id: EventId,
- pub local_event_seq: i64,
- pub outbox_operation_id: i64,
- pub outbox_event_id: i64,
- pub state: SdkMutationState,
- pub idempotency_digest_prefix: Option<String>,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Copy, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
-pub struct SdkExactLocation {
- pub latitude: f64,
- pub longitude: f64,
-}
-
-#[cfg(feature = "runtime")]
-impl SdkExactLocation {
- pub fn new(latitude: f64, longitude: f64) -> Self {
- Self {
- latitude,
- longitude,
- }
- }
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
-pub struct SdkPublicLocality {
- pub primary: String,
- pub city: Option<String>,
- pub region: Option<String>,
- pub country: Option<String>,
- pub geohash5: String,
-}
-
-#[cfg(feature = "runtime")]
-impl SdkPublicLocality {
- pub fn to_farm_public_location(&self) -> FarmPublicLocation {
- FarmPublicLocation {
- primary: self.primary.clone(),
- city: self.city.clone(),
- region: self.region.clone(),
- country: self.country.clone(),
- geohash: self.geohash5.clone(),
- }
- }
-
- pub fn to_listing_public_location(&self) -> OperationalListingPublicLocation {
- OperationalListingPublicLocation {
- primary: self.primary.clone(),
- city: self.city.clone(),
- region: self.region.clone(),
- country: self.country.clone(),
- geohash: self.geohash5.clone(),
- }
- }
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, serde::Serialize)]
-#[non_exhaustive]
-pub struct FarmPrivateLocationUpsertRequest {
- #[serde(serialize_with = "crate::actor_json::serialize_actor_context")]
- pub actor: Actor,
- pub farm_d_tag: String,
- pub exact_location: SdkExactLocation,
- pub label: Option<String>,
- pub updated_at: Option<RadrootsSdkTimestamp>,
-}
-
-#[cfg(feature = "runtime")]
-impl FarmPrivateLocationUpsertRequest {
- pub fn new(
- actor: Actor,
- farm_d_tag: impl Into<String>,
- exact_location: SdkExactLocation,
- ) -> Self {
- Self {
- actor,
- farm_d_tag: farm_d_tag.into(),
- exact_location,
- label: None,
- updated_at: None,
- }
- }
-
- pub fn with_label(mut self, label: impl Into<String>) -> Self {
- self.label = Some(label.into());
- self
- }
-
- pub fn with_updated_at(mut self, updated_at: RadrootsSdkTimestamp) -> Self {
- self.updated_at = Some(updated_at);
- self
- }
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
-#[serde(rename_all = "snake_case", tag = "kind", content = "value")]
-pub enum FarmPrivateLocationInput {
- Exact(SdkExactLocation),
- Locality(GeocoderLocalityQuery),
-}
-
-#[cfg(feature = "runtime")]
-impl FarmPrivateLocationInput {
- pub fn exact(location: SdkExactLocation) -> Self {
- Self::Exact(location)
- }
-
- pub fn city(city: impl Into<String>) -> Self {
- Self::Locality(GeocoderLocalityQuery::structured(city))
- }
-
- pub fn query(query: impl Into<String>) -> Self {
- Self::Locality(GeocoderLocalityQuery::query(query))
- }
-
- pub fn geonames_id(id: i64) -> Self {
- Self::Locality(GeocoderLocalityQuery::feature_id(id))
- }
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, serde::Serialize)]
-#[non_exhaustive]
-pub struct FarmPrivateLocationSetRequest {
- #[serde(serialize_with = "crate::actor_json::serialize_actor_context")]
- pub actor: Actor,
- pub farm_d_tag: String,
- pub input: FarmPrivateLocationInput,
- pub label: Option<String>,
- pub updated_at: Option<RadrootsSdkTimestamp>,
-}
-
-#[cfg(feature = "runtime")]
-impl FarmPrivateLocationSetRequest {
- pub fn new(
- actor: Actor,
- farm_d_tag: impl Into<String>,
- input: FarmPrivateLocationInput,
- ) -> Self {
- Self {
- actor,
- farm_d_tag: farm_d_tag.into(),
- input,
- label: None,
- updated_at: None,
- }
- }
-
- pub fn exact(
- actor: Actor,
- farm_d_tag: impl Into<String>,
- exact_location: SdkExactLocation,
- ) -> Self {
- Self::new(
- actor,
- farm_d_tag,
- FarmPrivateLocationInput::exact(exact_location),
- )
- }
-
- pub fn city(actor: Actor, farm_d_tag: impl Into<String>, city: impl Into<String>) -> Self {
- Self::new(actor, farm_d_tag, FarmPrivateLocationInput::city(city))
- }
-
- pub fn query(actor: Actor, farm_d_tag: impl Into<String>, query: impl Into<String>) -> Self {
- Self::new(actor, farm_d_tag, FarmPrivateLocationInput::query(query))
- }
-
- pub fn geonames_id(actor: Actor, farm_d_tag: impl Into<String>, id: i64) -> Self {
- Self::new(actor, farm_d_tag, FarmPrivateLocationInput::geonames_id(id))
- }
-
- pub fn with_label(mut self, label: impl Into<String>) -> Self {
- self.label = Some(label.into());
- self
- }
-
- pub fn with_updated_at(mut self, updated_at: RadrootsSdkTimestamp) -> Self {
- self.updated_at = Some(updated_at);
- self
- }
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, serde::Serialize)]
-#[non_exhaustive]
-pub struct FarmPrivateLocationClearRequest {
- #[serde(serialize_with = "crate::actor_json::serialize_actor_context")]
- pub actor: Actor,
- pub farm_d_tag: String,
-}
-
-#[cfg(feature = "runtime")]
-impl FarmPrivateLocationClearRequest {
- pub fn new(actor: Actor, farm_d_tag: impl Into<String>) -> Self {
- Self {
- actor,
- farm_d_tag: farm_d_tag.into(),
- }
- }
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
-pub struct FarmPrivateLocationClearReceipt {
- pub farm_addr: AddressableCoordinate,
- pub cleared: bool,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
-pub struct FarmPrivateLocationReceipt {
- pub farm_addr: AddressableCoordinate,
- pub farm_pubkey: String,
- pub farm_d_tag: String,
- pub label: Option<String>,
- pub exact_location: SdkExactLocation,
- pub public_locality: SdkPublicLocality,
- pub geonames_feature_id: Option<i64>,
- pub geonames_country_id: Option<String>,
- pub updated_at_ms: i64,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
-pub struct FarmPrivateLocationLookupCandidate {
- pub geonames_feature_id: i64,
- pub geonames_country_id: String,
- pub name: String,
- pub display_name: String,
- pub exact_location: SdkExactLocation,
- pub region: Option<String>,
- pub country: Option<String>,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
-pub struct FarmPrivateLocationLookupReceipt {
- pub farm_addr: AddressableCoordinate,
- pub farm_pubkey: String,
- pub farm_d_tag: String,
- pub input: FarmPrivateLocationInput,
- pub candidates: Vec<FarmPrivateLocationLookupCandidate>,
-}
-
-#[cfg(feature = "runtime")]
-#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
-#[serde(rename_all = "snake_case", tag = "status", content = "receipt")]
-pub enum FarmPrivateLocationSetResult {
- Stored(FarmPrivateLocationReceipt),
- NoMatch(FarmPrivateLocationLookupReceipt),
- Ambiguous(FarmPrivateLocationLookupReceipt),
-}
-
-#[cfg(feature = "runtime")]
-impl<'sdk> FarmsClient<'sdk> {
- pub fn prepare_publish(
- &self,
- request: FarmPreparePublishRequest,
- ) -> Result<FarmPublishPlan, RadrootsSdkError> {
- let created_at = self.resolved_created_at(request.created_at)?;
- farm_publish_plan(&request.actor, request.farm, created_at)
- }
-
- #[cfg(feature = "signer-adapters")]
- pub async fn enqueue_publish(
- &self,
- request: FarmEnqueuePublishRequest,
- ) -> Result<FarmEnqueueReceipt, RadrootsSdkError> {
- let FarmEnqueuePublishRequest {
- actor,
- farm,
- target_policy,
- idempotency_key,
- created_at,
- } = request;
- let prepare_request = FarmPreparePublishRequest {
- actor: actor.clone(),
- farm,
- created_at,
- };
- let plan = self.prepare_publish(prepare_request)?;
- self.enqueue_prepared_publish(&actor, plan, target_policy, idempotency_key)
- .await
- }
-
- pub async fn enqueue_publish_with_explicit_signer(
- &self,
- request: FarmEnqueuePublishRequest,
- signer: &dyn Signer,
- ) -> Result<FarmEnqueueReceipt, RadrootsSdkError> {
- let FarmEnqueuePublishRequest {
- actor,
- farm,
- target_policy,
- idempotency_key,
- created_at,
- } = request;
- let prepare_request = FarmPreparePublishRequest {
- actor: actor.clone(),
- farm,
- created_at,
- };
- let plan = self.prepare_publish(prepare_request)?;
- self.enqueue_prepared_publish_with_explicit_signer(
- &actor,
- plan,
- target_policy,
- idempotency_key,
- signer,
- )
- .await
- }
-
- #[cfg(feature = "signer-adapters")]
- pub async fn enqueue_prepared_publish(
- &self,
- actor: &Actor,
- plan: FarmPublishPlan,
- 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 {
- operation_kind: FARM_PUBLISH_OPERATION_KIND,
- actor,
- frozen_draft: &plan.frozen_draft,
- target_policy,
- satisfaction_policy: SatisfactionPolicy::AllAccepted,
- idempotency_key,
- },
- )
- .await?;
- Ok(farm_enqueue_receipt(metadata, enqueue))
- }
-
- pub async fn enqueue_prepared_publish_with_explicit_signer(
- &self,
- actor: &Actor,
- plan: FarmPublishPlan,
- target_policy: TargetPolicy,
- idempotency_key: Option<SdkIdempotencyKey>,
- signer: &dyn Signer,
- ) -> Result<FarmEnqueueReceipt, RadrootsSdkError> {
- let metadata = validate_farm_publish_plan(&plan)?;
- let enqueue = enqueue_signed_workflow(
- self.sdk,
- SdkWorkflowEnqueueRequest {
- operation_kind: FARM_PUBLISH_OPERATION_KIND,
- actor,
- frozen_draft: &plan.frozen_draft,
- target_policy,
- satisfaction_policy: SatisfactionPolicy::AllAccepted,
- idempotency_key,
- },
- signer,
- )
- .await?;
- Ok(farm_enqueue_receipt(metadata, enqueue))
- }
-
- pub async fn upsert_private_location(
- &self,
- request: FarmPrivateLocationUpsertRequest,
- ) -> Result<FarmPrivateLocationReceipt, RadrootsSdkError> {
- let geocoder = self.sdk.geonames().open_verified()?;
- self.upsert_private_location_with_geocoder(request, &geocoder)
- .await
- }
-
- pub async fn set_private_location(
- &self,
- request: FarmPrivateLocationSetRequest,
- ) -> Result<FarmPrivateLocationSetResult, RadrootsSdkError> {
- let geocoder = self.sdk.geonames().open_verified()?;
- self.set_private_location_with_geocoder(request, &geocoder)
- .await
- }
-
- pub async fn set_private_location_with_geocoder(
- &self,
- request: FarmPrivateLocationSetRequest,
- geocoder: &Geocoder,
- ) -> Result<FarmPrivateLocationSetResult, RadrootsSdkError> {
- match request.input {
- FarmPrivateLocationInput::Exact(exact_location) => {
- let upsert = FarmPrivateLocationUpsertRequest {
- actor: request.actor,
- farm_d_tag: request.farm_d_tag,
- exact_location,
- label: request.label,
- updated_at: request.updated_at,
- };
- self.upsert_private_location_with_geocoder(upsert, geocoder)
- .await
- .map(FarmPrivateLocationSetResult::Stored)
- }
- FarmPrivateLocationInput::Locality(locality_query) => {
- self.set_private_location_from_locality(
- request.actor,
- request.farm_d_tag,
- locality_query,
- request.label,
- request.updated_at,
- geocoder,
- )
- .await
- }
- }
- }
-
- pub async fn upsert_private_location_with_geocoder(
- &self,
- request: FarmPrivateLocationUpsertRequest,
- geocoder: &Geocoder,
- ) -> Result<FarmPrivateLocationReceipt, RadrootsSdkError> {
- require_farmer_actor(&request.actor, FARM_PRIVATE_LOCATION_OPERATION)?;
- validate_exact_location(request.exact_location)?;
- let label = normalize_private_location_label(request.label)?;
- let updated_at_ms = match request.updated_at {
- Some(updated_at) => sdk_timestamp_ms(updated_at)?,
- None => crate::runtime::sdk_now_ms(self.sdk)?,
- };
- let farm_addr = farm_addr(&request.actor, request.farm_d_tag.as_str())?;
- let reverse = nearest_geonames_locality(geocoder, request.exact_location)?;
- let public_locality = public_locality_from_reverse(request.exact_location, &reverse)?;
- let record = SdkPrivateFarmLocationRecord {
- farm_addr: farm_addr.clone(),
- farm_pubkey: request.actor.public_key().to_hex(),
- farm_d_tag: request.farm_d_tag,
- label,
- latitude: request.exact_location.latitude,
- longitude: request.exact_location.longitude,
- locality_primary: public_locality.primary.clone(),
- locality_city: public_locality.city.clone(),
- locality_region: public_locality.region.clone(),
- locality_country: public_locality.country.clone(),
- geohash5: public_locality.geohash5.clone(),
- geonames_feature_id: Some(reverse.id),
- geonames_country_id: Some(reverse.country_id.clone()),
- updated_at_ms,
- };
- self.sdk
- ._private_store
- .upsert_farm_location(&record)
- .await?;
- Ok(private_location_receipt_from_record(record))
- }
-
- async fn set_private_location_from_locality(
- &self,
- actor: Actor,
- farm_d_tag: String,
- locality_query: GeocoderLocalityQuery,
- label: Option<String>,
- updated_at: Option<RadrootsSdkTimestamp>,
- geocoder: &Geocoder,
- ) -> Result<FarmPrivateLocationSetResult, RadrootsSdkError> {
- require_farmer_actor(&actor, FARM_PRIVATE_LOCATION_OPERATION)?;
- let label = normalize_private_location_label(label)?;
- let farm_addr = farm_addr(&actor, farm_d_tag.as_str())?;
- let lookup = geocoder.locality(&locality_query)?;
- match lookup {
- GeocoderLocalityLookup::Unique { candidate } => {
- let exact_location = exact_location_from_locality_candidate(&candidate);
- validate_exact_location(exact_location)?;
- let public_locality = public_locality_from_candidate(exact_location, &candidate)?;
- let updated_at_ms = match updated_at {
- Some(updated_at) => sdk_timestamp_ms(updated_at)?,
- None => crate::runtime::sdk_now_ms(self.sdk)?,
- };
- let record = SdkPrivateFarmLocationRecord {
- farm_addr: farm_addr.clone(),
- farm_pubkey: actor.public_key().to_hex(),
- farm_d_tag,
- label,
- latitude: exact_location.latitude,
- longitude: exact_location.longitude,
- locality_primary: public_locality.primary.clone(),
- locality_city: public_locality.city.clone(),
- locality_region: public_locality.region.clone(),
- locality_country: public_locality.country.clone(),
- geohash5: public_locality.geohash5.clone(),
- geonames_feature_id: Some(candidate.id),
- geonames_country_id: Some(candidate.country_id.clone()),
- updated_at_ms,
- };
- self.sdk
- ._private_store
- .upsert_farm_location(&record)
- .await?;
- Ok(FarmPrivateLocationSetResult::Stored(
- private_location_receipt_from_record(record),
- ))
- }
- GeocoderLocalityLookup::NoMatch => Ok(FarmPrivateLocationSetResult::NoMatch(
- farm_private_location_lookup_receipt(
- farm_addr,
- actor.public_key().to_hex().as_str(),
- farm_d_tag,
- FarmPrivateLocationInput::Locality(locality_query),
- Vec::new(),
- ),
- )),
- GeocoderLocalityLookup::Ambiguous { candidates } => Ok(
- FarmPrivateLocationSetResult::Ambiguous(farm_private_location_lookup_receipt(
- farm_addr,
- actor.public_key().to_hex().as_str(),
- farm_d_tag,
- FarmPrivateLocationInput::Locality(locality_query),
- candidates
- .into_iter()
- .map(farm_private_location_candidate)
- .collect(),
- )),
- ),
- }
- }
-
- pub async fn private_location(
- &self,
- farm_addr: &AddressableCoordinate,
- ) -> Result<Option<FarmPrivateLocationReceipt>, RadrootsSdkError> {
- self.sdk
- ._private_store
- .farm_location(farm_addr)
- .await?
- .map(private_location_receipt_from_record)
- .map(Ok)
- .transpose()
- }
-
- pub async fn clear_private_location(
- &self,
- request: FarmPrivateLocationClearRequest,
- ) -> Result<FarmPrivateLocationClearReceipt, RadrootsSdkError> {
- require_farmer_actor(&request.actor, FARM_PRIVATE_LOCATION_OPERATION)?;
- let farm_addr = farm_addr(&request.actor, request.farm_d_tag.as_str())?;
- let cleared = self
- .sdk
- ._private_store
- .delete_farm_location(&farm_addr)
- .await?;
- Ok(FarmPrivateLocationClearReceipt { farm_addr, cleared })
- }
-
- fn resolved_created_at(
- &self,
- created_at: Option<RadrootsSdkTimestamp>,
- ) -> Result<RadrootsSdkTimestamp, RadrootsSdkError> {
- match created_at {
- Some(created_at) => Ok(created_at),
- None => self.sdk.now(),
- }
- }
-}
-
-#[cfg(feature = "runtime")]
-fn farm_enqueue_receipt(
- metadata: ValidatedFarmPublishPlanMetadata,
- enqueue: crate::workflow_runtime::SdkWorkflowEnqueueReceipt,
-) -> FarmEnqueueReceipt {
- FarmEnqueueReceipt {
- 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,
- outbox_event_id: enqueue.outbox_event_id,
- state: enqueue.state.into(),
- idempotency_digest_prefix: Some(enqueue.idempotency_digest_prefix),
- }
-}
-
-#[cfg(feature = "runtime")]
-struct ValidatedFarmPublishPlanMetadata {
- farm_addr: AddressableCoordinate,
- expected_event_id: EventId,
-}
-
-#[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 = EventId::parse(plan.frozen_draft.expected_event_id_hex())
- .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 = AddressableCoordinate::parse(format!(
- "{KIND_FARM}:{}:{d_tag}",
- plan.frozen_draft.expected_pubkey().to_hex()
- ))
- .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: &Actor,
- farm_value: Farm,
- created_at: RadrootsSdkTimestamp,
-) -> Result<FarmPublishPlan, RadrootsSdkError> {
- require_farmer_actor(actor, "farm.prepare_publish")?;
- let created_at_nostr = created_at.try_into_nostr_created_at()?;
- let parts =
- farm::build_draft(&farm_value).map_err(|error| RadrootsSdkError::InvalidRequest {
- message: format!("farm publish draft encode failed: {error}"),
- })?;
- let farm_addr = farm_addr(actor, farm_value.d_tag.as_str())
- .expect("validated farm d tag forms a farm address");
- let frozen_draft = EventDraft::new(
- FARM_PROFILE_CONTRACT_ID,
- parts.kind,
- created_at_nostr.into(),
- parts.tags,
- parts.content,
- actor.public_key().to_hex(),
- )
- .expect("validated farm publish draft freezes");
- let expected_event_id = EventId::parse(frozen_draft.expected_event_id_hex())
- .expect("frozen farm draft produces a valid event id");
- Ok(FarmPublishPlan {
- farm_addr,
- expected_event_id,
- frozen_draft,
- created_at,
- })
-}
-
-#[cfg(feature = "runtime")]
-fn require_farmer_actor(actor: &Actor, operation: &'static str) -> Result<(), RadrootsSdkError> {
- if actor.satisfies(AuthorRole::Farmer) {
- Ok(())
- } else {
- Err(RadrootsSdkError::UnauthorizedActor {
- operation: operation.to_owned(),
- reason: "missing role Farmer".to_owned(),
- })
- }
-}
-
-#[cfg(feature = "runtime")]
-fn farm_addr(actor: &Actor, d_tag: &str) -> Result<AddressableCoordinate, RadrootsSdkError> {
- AddressableCoordinate::parse(format!("{KIND_FARM}:{}:{d_tag}", actor.public_key())).map_err(
- |error| RadrootsSdkError::InvalidRequest {
- message: format!("farm address is invalid: {error}"),
- },
- )
-}
-
-#[cfg(feature = "runtime")]
-fn validate_exact_location(location: SdkExactLocation) -> Result<(), RadrootsSdkError> {
- if !location.latitude.is_finite()
- || !location.longitude.is_finite()
- || location.latitude < -90.0
- || location.latitude > 90.0
- || location.longitude < -180.0
- || location.longitude > 180.0
- {
- return Err(RadrootsSdkError::InvalidRequest {
- message: "farm exact location coordinates are outside valid latitude/longitude bounds"
- .to_owned(),
- });
- }
- Ok(())
-}
-
-#[cfg(feature = "runtime")]
-fn sdk_timestamp_ms(timestamp: RadrootsSdkTimestamp) -> Result<i64, RadrootsSdkError> {
- let seconds = timestamp.unix_seconds();
- let millis = seconds
- .checked_mul(1_000)
- .ok_or(RadrootsSdkError::TimestampOutOfRange { value: seconds })?;
- i64::try_from(millis).map_err(|_| RadrootsSdkError::TimestampOutOfRange { value: seconds })
-}
-
-#[cfg(feature = "runtime")]
-fn nearest_geonames_locality(
- geocoder: &Geocoder,
- exact_location: SdkExactLocation,
-) -> Result<GeocoderReverseResult, RadrootsSdkError> {
- let mut results = geocoder.reverse(
- GeocoderPoint {
- lat: exact_location.latitude,
- lng: exact_location.longitude,
- },
- Some(GeocoderReverseOptions {
- limit: 1,
- degree_offset: 0.5,
- }),
- )?;
- results.pop().ok_or_else(|| RadrootsSdkError::GeoNames {
- kind: crate::RadrootsSdkGeoNamesErrorKind::Lookup,
- message: "GeoNames reverse lookup returned no public locality".to_owned(),
- })
-}
-
-#[cfg(feature = "runtime")]
-fn public_locality_from_reverse(
- exact_location: SdkExactLocation,
- reverse: &GeocoderReverseResult,
-) -> Result<SdkPublicLocality, RadrootsSdkError> {
- let primary = required_public_string(reverse.name.as_str(), "GeoNames locality name")?;
- let country = optional_public_string(reverse.country_name.as_deref())
- .or_else(|| Some(reverse.country_id.clone()));
- Ok(SdkPublicLocality {
- primary: primary.clone(),
- city: Some(primary),
- region: optional_public_string(reverse.admin1_name.as_deref()),
- country,
- geohash5: geohash5(exact_location)?,
- })
-}
-
-#[cfg(feature = "runtime")]
-fn public_locality_from_candidate(
- exact_location: SdkExactLocation,
- candidate: &GeocoderLocalityCandidate,
-) -> Result<SdkPublicLocality, RadrootsSdkError> {
- let primary = required_public_string(candidate.name.as_str(), "GeoNames locality name")?;
- let country = optional_public_string(candidate.country_name.as_deref())
- .or_else(|| Some(candidate.country_id.clone()));
- Ok(SdkPublicLocality {
- primary: primary.clone(),
- city: Some(primary),
- region: optional_public_string(candidate.admin1_name.as_deref()),
- country,
- geohash5: geohash5(exact_location)?,
- })
-}
-
-#[cfg(feature = "runtime")]
-fn exact_location_from_locality_candidate(
- candidate: &GeocoderLocalityCandidate,
-) -> SdkExactLocation {
- SdkExactLocation {
- latitude: candidate.point.lat,
- longitude: candidate.point.lng,
- }
-}
-
-#[cfg(feature = "runtime")]
-fn normalize_private_location_label(
- label: Option<String>,
-) -> Result<Option<String>, RadrootsSdkError> {
- match label {
- Some(label) => {
- let label = label.trim();
- if label.is_empty() {
- return Err(RadrootsSdkError::InvalidRequest {
- message: "farm private location label must not be empty".to_owned(),
- });
- }
- Ok(Some(label.to_owned()))
- }
- None => Ok(None),
- }
-}
-
-#[cfg(feature = "runtime")]
-fn required_public_string(value: &str, label: &str) -> Result<String, RadrootsSdkError> {
- let trimmed = value.trim();
- if trimmed.is_empty() {
- return Err(RadrootsSdkError::GeoNames {
- kind: crate::RadrootsSdkGeoNamesErrorKind::Lookup,
- message: format!("{label} must not be empty"),
- });
- }
- Ok(trimmed.to_owned())
-}
-
-#[cfg(feature = "runtime")]
-fn optional_public_string(value: Option<&str>) -> Option<String> {
- value
- .map(str::trim)
- .filter(|value| !value.is_empty())
- .map(str::to_owned)
-}
-
-#[cfg(feature = "runtime")]
-fn geohash5(location: SdkExactLocation) -> Result<String, RadrootsSdkError> {
- validate_exact_location(location)?;
- let mut latitude_min = -90.0;
- let mut latitude_max = 90.0;
- let mut longitude_min = -180.0;
- let mut longitude_max = 180.0;
- let mut even = true;
- let mut bit_count = 0;
- let mut value = 0usize;
- let mut geohash = String::with_capacity(GEOHASH5_LEN);
- while geohash.len() < GEOHASH5_LEN {
- value <<= 1;
- if even {
- let middle = (longitude_min + longitude_max) / 2.0;
- if location.longitude >= middle {
- value |= 1;
- longitude_min = middle;
- } else {
- longitude_max = middle;
- }
- } else {
- let middle = (latitude_min + latitude_max) / 2.0;
- if location.latitude >= middle {
- value |= 1;
- latitude_min = middle;
- } else {
- latitude_max = middle;
- }
- }
- even = !even;
- bit_count += 1;
- if bit_count == 5 {
- geohash.push(GEOHASH_BASE32[value] as char);
- bit_count = 0;
- value = 0;
- }
- }
- Ok(geohash)
-}
-
-#[cfg(feature = "runtime")]
-fn private_location_receipt_from_record(
- record: SdkPrivateFarmLocationRecord,
-) -> FarmPrivateLocationReceipt {
- FarmPrivateLocationReceipt {
- farm_addr: record.farm_addr,
- farm_pubkey: record.farm_pubkey,
- farm_d_tag: record.farm_d_tag,
- label: record.label,
- exact_location: SdkExactLocation {
- latitude: record.latitude,
- longitude: record.longitude,
- },
- public_locality: SdkPublicLocality {
- primary: record.locality_primary,
- city: record.locality_city,
- region: record.locality_region,
- country: record.locality_country,
- geohash5: record.geohash5,
- },
- geonames_feature_id: record.geonames_feature_id,
- geonames_country_id: record.geonames_country_id,
- updated_at_ms: record.updated_at_ms,
- }
-}
-
-#[cfg(feature = "runtime")]
-fn farm_private_location_lookup_receipt(
- farm_addr: AddressableCoordinate,
- farm_pubkey: &str,
- farm_d_tag: String,
- input: FarmPrivateLocationInput,
- candidates: Vec<FarmPrivateLocationLookupCandidate>,
-) -> FarmPrivateLocationLookupReceipt {
- FarmPrivateLocationLookupReceipt {
- farm_addr,
- farm_pubkey: farm_pubkey.to_owned(),
- farm_d_tag,
- input,
- candidates,
- }
-}
-
-#[cfg(feature = "runtime")]
-fn farm_private_location_candidate(
- candidate: GeocoderLocalityCandidate,
-) -> FarmPrivateLocationLookupCandidate {
- let exact_location = exact_location_from_locality_candidate(&candidate);
- let region = optional_public_string(candidate.admin1_name.as_deref());
- let country = optional_public_string(candidate.country_name.as_deref())
- .or_else(|| Some(candidate.country_id.clone()));
- FarmPrivateLocationLookupCandidate {
- geonames_feature_id: candidate.id,
- geonames_country_id: candidate.country_id,
- name: candidate.name,
- display_name: candidate.display_name,
- exact_location,
- region,
- country,
- }
-}
-
-#[cfg(all(test, feature = "runtime", feature = "signer-adapters"))]
-#[path = "../tests/unit/farms_runtime_tests.rs"]
-mod tests;
diff --git a/crates/sdk/tests/farms_runtime.rs b/crates/sdk/tests/farms_runtime.rs
@@ -1,1303 +0,0 @@
-#![cfg(feature = "runtime")]
-
-use radroots_event::{
- contract::AuthorRole,
- envelope::kind::{KIND_FARM, KIND_PROFILE},
- farm::Farm,
- id::AddressableCoordinate,
-};
-use radroots_event_store::RadrootsEventStore;
-use radroots_outbox::{
- RadrootsOutbox, RadrootsOutboxDeliveryPlanStatus, RadrootsOutboxDeliveryTargetStatus,
- RadrootsOutboxEventState,
-};
-use radroots_sdk::{
- FARM_PUBLISH_OPERATION_KIND, FarmEnqueuePublishRequest, FarmPreparePublishRequest,
- FarmPrivateLocationClearRequest, FarmPrivateLocationInput, FarmPrivateLocationLookupCandidate,
- FarmPrivateLocationLookupReceipt, FarmPrivateLocationReceipt, FarmPrivateLocationSetRequest,
- FarmPrivateLocationSetResult, FarmPrivateLocationUpsertRequest, Geocoder,
- GeocoderLocalityQuery, MultiTargetProfile, NostrProfile, NostrRelayUrlPolicy,
- PushOutboxEventState, PushOutboxRequest, PushOutboxTargetOutcomeKind, RadrootsClient,
- RadrootsSdkError, RadrootsSdkErrorClass, RadrootsSdkGeoNamesErrorKind,
- RadrootsSdkRecoveryAction, RadrootsSdkTimestamp, ReticulumProfile, SdkExactLocation,
- SdkIdempotencyKey, SdkMutationState, SdkPublicLocality, StorageStatusRequest, TargetPolicy,
- TargetSet, TransportProfile,
-};
-use radroots_signing::{Actor, actor::ActorSource};
-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 FARM_A_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAAAA";
-const FARM_B_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAAAQ";
-const FARM_C_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAAAg";
-const FARM_D_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAAAw";
-const FARM_E_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAABA";
-const FARM_F_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAABQ";
-const RELAY: &str = "wss://relay.example.com";
-const RELAY_B: &str = "wss://relay-b.example.com";
-
-fn farmer_pubkey() -> &'static str {
- fixture_alice_pubkey()
-}
-
-fn other_pubkey() -> &'static str {
- fixture_bob_pubkey()
-}
-
-fn farmer_actor() -> Actor {
- Actor::from_public_key_hex(
- farmer_pubkey(),
- ActorSource::ExplicitPublicKey,
- [AuthorRole::Farmer],
- )
- .expect("actor")
-}
-
-fn non_farmer_actor() -> Actor {
- Actor::from_public_key_hex(
- farmer_pubkey(),
- ActorSource::ExplicitPublicKey,
- [AuthorRole::Buyer],
- )
- .expect("actor")
-}
-
-fn farm(d_tag: &str, name: &str) -> Farm {
- Farm {
- d_tag: d_tag.to_owned(),
- name: name.to_owned(),
- about: Some("Vegetable farm".to_owned()),
- website: Some("https://example.invalid/north-farm".to_owned()),
- picture: None,
- banner: None,
- location: None,
- tags: Some(vec!["vegetables".to_owned(), "local".to_owned()]),
- }
-}
-
-fn farm_addr(actor: &Actor, d_tag: &str) -> AddressableCoordinate {
- AddressableCoordinate::parse(format!("{KIND_FARM}:{}:{d_tag}", actor.public_key()))
- .expect("farm addr")
-}
-
-fn stored_location(result: FarmPrivateLocationSetResult) -> FarmPrivateLocationReceipt {
- let FarmPrivateLocationSetResult::Stored(receipt) = result else {
- panic!("expected stored location");
- };
- receipt
-}
-
-async fn directory_sdk() -> (tempfile::TempDir, RadrootsClient) {
- directory_sdk_with_relays(&[RELAY]).await
-}
-
-async fn directory_sdk_with_relays(relays: &[&str]) -> (tempfile::TempDir, RadrootsClient) {
- let tempdir = tempfile::tempdir().expect("tempdir");
- let mut builder = RadrootsClient::builder()
- .directory_storage(tempdir.path().join("sdk"))
- .fixed_clock(RadrootsSdkTimestamp::from_unix_seconds(1_700_000_000));
- if !relays.is_empty() {
- builder = builder.transport_profile(TransportProfile::nostr(
- NostrProfile::new(relays.iter().copied(), NostrRelayUrlPolicy::Public)
- .expect("Nostr profile"),
- ));
- }
- let sdk = builder.build().await.expect("sdk");
- (tempdir, sdk)
-}
-
-async fn multi_target_directory_sdk() -> (tempfile::TempDir, RadrootsClient) {
- let tempdir = tempfile::tempdir().expect("tempdir");
- let sdk = RadrootsClient::builder()
- .directory_storage(tempdir.path().join("sdk"))
- .fixed_clock(RadrootsSdkTimestamp::from_unix_seconds(1_700_000_000))
- .transport_profile(TransportProfile::multi_target(MultiTargetProfile::new(
- NostrProfile::new([RELAY], NostrRelayUrlPolicy::Public).expect("Nostr profile"),
- ReticulumProfile::deferred_until_implemented(),
- )))
- .build()
- .await
- .expect("sdk");
- (tempdir, sdk)
-}
-
-async fn fixture_geocoder(tempdir: &tempfile::TempDir) -> Geocoder {
- let path = tempdir.path().join("geonames-fixture.db");
- let options = SqliteConnectOptions::new()
- .filename(&path)
- .create_if_missing(true);
- let pool = SqlitePoolOptions::new()
- .max_connections(1)
- .connect_with(options)
- .await
- .expect("geonames fixture pool");
- sqlx::raw_sql(
- r#"
- CREATE TABLE countries(
- id TEXT,
- name TEXT,
- PRIMARY KEY (id)
- );
- CREATE TABLE admin1(
- country_id TEXT,
- id INTEGER,
- name TEXT,
- PRIMARY KEY (country_id, id)
- );
- CREATE TABLE features(
- id INTEGER,
- name TEXT,
- country_id TEXT,
- admin1_id INTEGER,
- PRIMARY KEY (id)
- );
- CREATE TABLE coordinates(
- feature_id INTEGER,
- latitude REAL,
- longitude REAL,
- PRIMARY KEY (feature_id)
- );
- CREATE INDEX coordinates_lat_lng ON coordinates (latitude, longitude);
- CREATE VIEW geonames AS
- SELECT
- features.id,
- features.name,
- admin1.id AS admin1_id,
- admin1.name AS admin1_name,
- countries.id AS country_id,
- countries.name AS country_name,
- coordinates.latitude AS latitude,
- coordinates.longitude AS longitude
- FROM features
- LEFT JOIN countries ON features.country_id = countries.id
- LEFT JOIN admin1 ON features.country_id = admin1.country_id AND features.admin1_id = admin1.id
- JOIN coordinates ON features.id = coordinates.feature_id;
- INSERT INTO countries (id, name) VALUES ('FX', 'Fixture Country');
- INSERT INTO countries (id, name) VALUES ('CA', 'Canada');
- INSERT INTO countries (id, name) VALUES ('US', 'United States');
- INSERT INTO admin1 (country_id, id, name) VALUES ('FX', 1, 'Fixture Region');
- INSERT INTO admin1 (country_id, id, name) VALUES ('CA', 2, 'British Columbia');
- INSERT INTO admin1 (country_id, id, name) VALUES ('CA', 3, 'Prairie Region');
- INSERT INTO admin1 (country_id, id, name) VALUES ('US', 4, 'River Region');
- INSERT INTO features (id, name, country_id, admin1_id) VALUES (1, 'Fixture Town', 'FX', 1);
- INSERT INTO features (id, name, country_id, admin1_id) VALUES (3001, 'Fixture Victoria', 'CA', 2);
- INSERT INTO features (id, name, country_id, admin1_id) VALUES (3002, 'Shared Market', 'CA', 2);
- INSERT INTO features (id, name, country_id, admin1_id) VALUES (3003, 'Shared Market', 'CA', 3);
- INSERT INTO features (id, name, country_id, admin1_id) VALUES (3004, 'Identifier Grove', 'CA', 2);
- INSERT INTO features (id, name, country_id, admin1_id) VALUES (3005, 'Query Hamlet', 'US', 4);
- INSERT INTO coordinates (feature_id, latitude, longitude) VALUES (1, 12.25, -34.50);
- INSERT INTO coordinates (feature_id, latitude, longitude) VALUES (3001, 48.4359, -123.35155);
- INSERT INTO coordinates (feature_id, latitude, longitude) VALUES (3002, 48.7, -123.2);
- INSERT INTO coordinates (feature_id, latitude, longitude) VALUES (3003, 50.2, -110.4);
- INSERT INTO coordinates (feature_id, latitude, longitude) VALUES (3004, 48.9, -123.4);
- INSERT INTO coordinates (feature_id, latitude, longitude) VALUES (3005, 39.25, -77.5);
- "#,
- )
- .execute(&pool)
- .await
- .expect("seed geonames fixture");
- pool.close().await;
- Geocoder::open_path(path).expect("open geonames fixture")
-}
-
-#[tokio::test]
-async fn farm_prepare_publish_is_side_effect_free() {
- let (_tempdir, sdk) = directory_sdk().await;
- 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.expected_event_id().to_hex(),
- prepared.frozen_draft().expected_event_id_hex()
- );
- assert_eq!(
- prepared.farm_addr().as_str(),
- format!("{KIND_FARM}:{}:{FARM_A_D_TAG}", farmer_pubkey())
- );
-
- let paths = sdk.storage_paths().expect("paths");
- let event_store = RadrootsEventStore::open_file(&paths.runtime_path)
- .await
- .expect("event store");
- assert_eq!(
- event_store
- .status_summary()
- .await
- .expect("event store status")
- .total_events,
- 0
- );
- assert!(
- event_store
- .raw_event(&prepared.expected_event_id().to_hex())
- .await
- .expect("event lookup")
- .is_none()
- );
- let outbox = RadrootsOutbox::open_file(&paths.runtime_path)
- .await
- .expect("outbox");
- assert!(
- outbox
- .claim_next_ready_event("worker", "claim", 2_000, 1_700_000_000_000)
- .await
- .expect("claim")
- .is_none()
- );
-}
-
-#[tokio::test]
-async fn farm_prepare_publish_rejects_non_farmer_actor() {
- let (_tempdir, sdk) = directory_sdk().await;
- let request =
- FarmPreparePublishRequest::new(non_farmer_actor(), farm(FARM_B_D_TAG, "North Farm"));
-
- let error = sdk
- .farms()
- .prepare_publish(request)
- .expect_err("non farmer");
-
- assert!(matches!(error, RadrootsSdkError::UnauthorizedActor { .. }));
-}
-
-#[tokio::test]
-async fn farm_private_location_upsert_stores_exact_location_and_public_locality_without_events() {
- let (tempdir, sdk) = directory_sdk().await;
- let geocoder = fixture_geocoder(&tempdir).await;
- let request = FarmPrivateLocationUpsertRequest::new(
- farmer_actor(),
- FARM_A_D_TAG,
- SdkExactLocation::new(12.26, -34.51),
- )
- .with_updated_at(RadrootsSdkTimestamp::from_unix_seconds(1_700_000_123));
-
- let receipt = sdk
- .farms()
- .upsert_private_location_with_geocoder(request, &geocoder)
- .await
- .expect("upsert private location");
-
- assert_eq!(
- receipt.farm_addr.as_str(),
- format!("{KIND_FARM}:{}:{FARM_A_D_TAG}", farmer_pubkey())
- );
- 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));
- assert_eq!(receipt.public_locality.primary, "Fixture Town");
- assert_eq!(
- receipt.public_locality.city.as_deref(),
- Some("Fixture Town")
- );
- assert_eq!(
- receipt.public_locality.region.as_deref(),
- Some("Fixture Region")
- );
- assert_eq!(
- receipt.public_locality.country.as_deref(),
- Some("Fixture Country")
- );
- assert_eq!(receipt.public_locality.geohash5, "e4pmw");
- assert_eq!(receipt.geonames_feature_id, Some(1));
- assert_eq!(receipt.geonames_country_id.as_deref(), Some("FX"));
- assert_eq!(receipt.updated_at_ms, 1_700_000_123_000);
- let farm_public = receipt.public_locality.to_farm_public_location();
- assert_eq!(farm_public.primary, "Fixture Town");
- assert_eq!(farm_public.geohash, "e4pmw");
- let listing_public = receipt.public_locality.to_listing_public_location();
- assert_eq!(listing_public.primary, "Fixture Town");
- assert_eq!(listing_public.geohash, "e4pmw");
-
- let stored = sdk
- .farms()
- .private_location(&receipt.farm_addr)
- .await
- .expect("private location read")
- .expect("stored private location");
- assert_eq!(stored, receipt);
- let status = sdk
- .storage_status(StorageStatusRequest::new())
- .await
- .expect("status");
- assert_eq!(status.private_store.farm_private_locations, 1);
- assert_eq!(status.event_store.total_events, 0);
- assert_eq!(status.outbox.total_events, 0);
-
- let clock_receipt = sdk
- .farms()
- .upsert_private_location_with_geocoder(
- FarmPrivateLocationUpsertRequest::new(
- farmer_actor(),
- FARM_B_D_TAG,
- SdkExactLocation::new(12.26, -34.51),
- ),
- &geocoder,
- )
- .await
- .expect("upsert with sdk clock");
- assert_eq!(clock_receipt.updated_at_ms, 1_700_000_000_000);
-}
-
-#[tokio::test]
-async fn farm_private_location_set_resolves_forward_inputs_and_preserves_no_mutation_failures() {
- let (tempdir, sdk) = directory_sdk().await;
- let geocoder = fixture_geocoder(&tempdir).await;
- let actor = farmer_actor();
-
- let exact = stored_location(
- sdk.farms()
- .set_private_location_with_geocoder(
- FarmPrivateLocationSetRequest::exact(
- actor.clone(),
- FARM_A_D_TAG,
- SdkExactLocation::new(12.26, -34.51),
- )
- .with_label(" main pickup point ")
- .with_updated_at(RadrootsSdkTimestamp::from_unix_seconds(1_700_000_200)),
- &geocoder,
- )
- .await
- .expect("exact set"),
- );
- assert_eq!(exact.label.as_deref(), Some("main pickup point"));
- assert_eq!(exact.geonames_feature_id, Some(1));
- assert_eq!(exact.updated_at_ms, 1_700_000_200_000);
-
- let city = stored_location(
- sdk.farms()
- .set_private_location_with_geocoder(
- FarmPrivateLocationSetRequest::city(
- actor.clone(),
- FARM_B_D_TAG,
- "Fixture Victoria",
- )
- .with_updated_at(RadrootsSdkTimestamp::from_unix_seconds(1_700_000_201)),
- &geocoder,
- )
- .await
- .expect("city set"),
- );
- assert_eq!(city.geonames_feature_id, Some(3001));
- assert_eq!(city.public_locality.primary, "Fixture Victoria");
- assert_eq!(
- city.public_locality.region.as_deref(),
- Some("British Columbia")
- );
- assert_eq!(city.public_locality.country.as_deref(), Some("Canada"));
- assert_eq!(
- city.exact_location,
- SdkExactLocation::new(48.4359, -123.35155)
- );
-
- let query = stored_location(
- sdk.farms()
- .set_private_location_with_geocoder(
- FarmPrivateLocationSetRequest::query(
- actor.clone(),
- FARM_C_D_TAG,
- "Fixture Victoria, BC, CA",
- ),
- &geocoder,
- )
- .await
- .expect("query set"),
- );
- assert_eq!(query.geonames_feature_id, Some(3001));
-
- let selected = stored_location(
- sdk.farms()
- .set_private_location_with_geocoder(
- FarmPrivateLocationSetRequest::geonames_id(actor.clone(), FARM_D_D_TAG, 3004),
- &geocoder,
- )
- .await
- .expect("id set"),
- );
- assert_eq!(selected.geonames_feature_id, Some(3004));
- assert_eq!(selected.public_locality.primary, "Identifier Grove");
-
- let narrowed = stored_location(
- sdk.farms()
- .set_private_location_with_geocoder(
- FarmPrivateLocationSetRequest::new(
- actor.clone(),
- FARM_E_D_TAG,
- FarmPrivateLocationInput::Locality(
- GeocoderLocalityQuery::structured("Shared Market")
- .with_region("Prairie Region")
- .with_country("CA"),
- ),
- ),
- &geocoder,
- )
- .await
- .expect("structured narrowed set"),
- );
- assert_eq!(narrowed.geonames_feature_id, Some(3003));
- assert_eq!(
- narrowed.public_locality.region.as_deref(),
- Some("Prairie Region")
- );
-
- let before_failure_status = sdk
- .storage_status(StorageStatusRequest::new())
- .await
- .expect("pre-failure status")
- .private_store
- .farm_private_locations;
-
- let ambiguous = sdk
- .farms()
- .set_private_location_with_geocoder(
- FarmPrivateLocationSetRequest::new(
- actor.clone(),
- FARM_F_D_TAG,
- FarmPrivateLocationInput::Locality(
- GeocoderLocalityQuery::structured("Shared Market").with_country("CA"),
- ),
- ),
- &geocoder,
- )
- .await
- .expect("ambiguous set");
- let FarmPrivateLocationSetResult::Ambiguous(ambiguous) = ambiguous else {
- panic!("expected ambiguous result");
- };
- assert_eq!(
- ambiguous
- .candidates
- .iter()
- .map(|candidate| candidate.geonames_feature_id)
- .collect::<Vec<_>>(),
- vec![3002, 3003]
- );
-
- let missing = sdk
- .farms()
- .set_private_location_with_geocoder(
- FarmPrivateLocationSetRequest::query(actor.clone(), FARM_F_D_TAG, "Missing Market, CA"),
- &geocoder,
- )
- .await
- .expect("missing set");
- assert!(matches!(missing, FarmPrivateLocationSetResult::NoMatch(_)));
-
- assert_eq!(
- sdk.storage_status(StorageStatusRequest::new())
- .await
- .expect("post-failure status")
- .private_store
- .farm_private_locations,
- before_failure_status
- );
- assert_eq!(
- sdk.farms()
- .private_location(&farm_addr(&actor, FARM_F_D_TAG))
- .await
- .expect("failure location lookup"),
- None
- );
-
- assert!(matches!(
- sdk.farms()
- .set_private_location_with_geocoder(
- FarmPrivateLocationSetRequest::city(actor, FARM_F_D_TAG, "Fixture Victoria")
- .with_label(" "),
- &geocoder,
- )
- .await,
- Err(RadrootsSdkError::InvalidRequest { .. })
- ));
-}
-
-#[tokio::test]
-async fn farm_private_location_requires_farmer_role_and_valid_coordinates() {
- let (tempdir, sdk) = directory_sdk().await;
- let geocoder = fixture_geocoder(&tempdir).await;
- let non_farmer = sdk
- .farms()
- .upsert_private_location_with_geocoder(
- FarmPrivateLocationUpsertRequest::new(
- non_farmer_actor(),
- FARM_B_D_TAG,
- SdkExactLocation::new(12.26, -34.51),
- ),
- &geocoder,
- )
- .await
- .expect_err("non farmer");
- assert!(matches!(
- non_farmer,
- RadrootsSdkError::UnauthorizedActor { .. }
- ));
-
- let invalid = sdk
- .farms()
- .upsert_private_location_with_geocoder(
- FarmPrivateLocationUpsertRequest::new(
- farmer_actor(),
- FARM_B_D_TAG,
- SdkExactLocation::new(91.0, -34.51),
- ),
- &geocoder,
- )
- .await
- .expect_err("invalid coordinates");
- assert!(matches!(invalid, RadrootsSdkError::InvalidRequest { .. }));
-}
-
-#[tokio::test]
-async fn farm_private_location_requires_configured_geonames_for_default_upsert() {
- let (_tempdir, sdk) = directory_sdk().await;
- let error = sdk
- .farms()
- .upsert_private_location(FarmPrivateLocationUpsertRequest::new(
- farmer_actor(),
- FARM_C_D_TAG,
- SdkExactLocation::new(12.26, -34.51),
- ))
- .await
- .expect_err("missing geonames config");
-
- assert!(matches!(
- error,
- RadrootsSdkError::GeoNames {
- kind: RadrootsSdkGeoNamesErrorKind::Configuration,
- ..
- }
- ));
- assert_eq!(error.class(), RadrootsSdkErrorClass::Configuration);
- assert_eq!(
- error.recovery_actions(),
- vec![RadrootsSdkRecoveryAction::ConfigureGeoNamesCache]
- );
-}
-
-#[tokio::test]
-async fn farm_enqueue_publish_stores_event_and_queues_signed_outbox_without_profile_event() {
- let (_tempdir, sdk) = directory_sdk().await;
- let request = FarmEnqueuePublishRequest::new(
- farmer_actor(),
- farm(FARM_B_D_TAG, "North Farm"),
- TargetPolicy::default_profile(),
- )
- .try_with_idempotency_key("01890f0e-6c00-7000-8000-000000000229")
- .expect("idempotency key");
- let prepared = sdk
- .farms()
- .prepare_publish(FarmPreparePublishRequest::new(
- farmer_actor(),
- farm(FARM_B_D_TAG, "North Farm"),
- ))
- .expect("prepared");
- let receipt = sdk
- .farms()
- .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.signed_event_id, receipt.expected_event_id);
- 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);
- assert_eq!(receipt.state, SdkMutationState::StoredAndQueued);
- assert!(receipt.idempotency_digest_prefix.is_some());
-
- let paths = sdk.storage_paths().expect("paths");
- let event_store = RadrootsEventStore::open_file(&paths.runtime_path)
- .await
- .expect("event store");
- let status = event_store
- .status_summary()
- .await
- .expect("event store status");
- assert_eq!(status.total_events, 1);
- let stored_event = event_store
- .valid_event(&receipt.signed_event_id.to_hex())
- .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!(
- stored_event.contract_id.as_deref(),
- Some("radroots.farm.profile.v1")
- );
-
- let outbox = RadrootsOutbox::open_file(&paths.runtime_path)
- .await
- .expect("outbox");
- let outbox_event = outbox
- .get_event(receipt.outbox_event_id)
- .await
- .expect("outbox event")
- .expect("outbox event");
- assert_eq!(outbox_event.state, RadrootsOutboxEventState::Signed);
- assert_eq!(outbox_event.draft.kind_u32(), KIND_FARM);
- assert!(outbox_event.signed_event.is_some());
-}
-
-#[tokio::test]
-async fn farm_enqueue_publish_returns_sanitized_signer_errors_before_mutation() {
- let (_tempdir, sdk) = directory_sdk().await;
- let request = FarmEnqueuePublishRequest::new(
- farmer_actor(),
- farm(FARM_C_D_TAG, "North Farm"),
- TargetPolicy::default_profile(),
- )
- .try_with_idempotency_key("01890f0e-6c00-7000-8000-000000000238")
- .expect("idempotency key");
- let error = sdk
- .farms()
- .enqueue_publish_with_explicit_signer(request, &FixtureSigner::new(other_pubkey()))
- .await
- .expect_err("signer error");
- let message = error.to_string();
-
- assert!(matches!(error, RadrootsSdkError::UnauthorizedActor { .. }));
- assert!(!message.contains("raw"));
- assert!(!message.contains("ffff"));
-
- let paths = sdk.storage_paths().expect("paths");
- let event_store = RadrootsEventStore::open_file(&paths.runtime_path)
- .await
- .expect("event store");
- assert_eq!(
- event_store
- .status_summary()
- .await
- .expect("event store status")
- .total_events,
- 0
- );
- let outbox = RadrootsOutbox::open_file(&paths.runtime_path)
- .await
- .expect("outbox");
- assert!(
- outbox
- .claim_next_ready_event("worker", "claim", 2_000, 1_700_000_000_000)
- .await
- .expect("claim")
- .is_none()
- );
-}
-
-#[tokio::test]
-async fn farm_enqueue_publish_uses_explicit_idempotency_key_across_equivalent_target_order() {
- let (_tempdir, sdk) = directory_sdk().await;
- let first = FarmEnqueuePublishRequest::new(
- farmer_actor(),
- farm(FARM_D_D_TAG, "North Farm"),
- TargetPolicy::default_profile(),
- )
- .try_with_nostr_targets([RELAY_B, RELAY], NostrRelayUrlPolicy::Public)
- .expect("first transport targets")
- .try_with_idempotency_key("01890f0e-6c00-7000-8000-00000000022d")
- .expect("first idempotency key");
- let second = FarmEnqueuePublishRequest::new(
- farmer_actor(),
- farm(FARM_D_D_TAG, "North Farm"),
- TargetPolicy::explicit(
- TargetSet::nostr_relays([RELAY, RELAY_B], NostrRelayUrlPolicy::Public)
- .expect("second transport targets"),
- ),
- )
- .try_with_idempotency_key("01890f0e-6c00-7000-8000-00000000022d")
- .expect("second idempotency key");
-
- let first_receipt = sdk
- .farms()
- .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_pubkey()))
- .await
- .expect("second enqueue");
-
- assert_eq!(
- first_receipt.outbox_event_id,
- second_receipt.outbox_event_id
- );
- assert_eq!(
- first_receipt.idempotency_digest_prefix,
- second_receipt.idempotency_digest_prefix
- );
- assert_eq!(second_receipt.state, SdkMutationState::StoredAndQueued);
-
- let paths = sdk.storage_paths().expect("paths");
- let outbox = RadrootsOutbox::open_file(&paths.runtime_path)
- .await
- .expect("outbox");
- let relay_urls = outbox
- .delivery_targets(first_receipt.outbox_event_id)
- .await
- .expect("delivery targets")
- .into_iter()
- .map(|target| target.endpoint_uri.to_string())
- .collect::<Vec<_>>();
- assert_eq!(relay_urls, vec![RELAY_B.to_owned(), RELAY.to_owned()]);
-}
-
-#[tokio::test]
-async fn farm_enqueue_publish_pushes_queued_event_with_mock_relay_sync() {
- let (_tempdir, sdk) = directory_sdk().await;
- let enqueue_request = FarmEnqueuePublishRequest::new(
- farmer_actor(),
- farm(FARM_D_D_TAG, "Sync Farm"),
- TargetPolicy::default_profile(),
- )
- .try_with_nostr_targets([RELAY], NostrRelayUrlPolicy::Public)
- .expect("transport targets")
- .try_with_idempotency_key("01890f0e-6c00-7000-8000-00000000022e")
- .expect("idempotency key");
- let enqueue_receipt = sdk
- .farms()
- .enqueue_publish_with_explicit_signer(enqueue_request, &FixtureSigner::new(farmer_pubkey()))
- .await
- .expect("enqueue");
- let adapter = RadrootsMockRelayPublishAdapter::new();
-
- let push_receipt = sdk
- .sync()
- .push_outbox_with_transport(
- &RadrootsNostrTransport::new(&adapter),
- PushOutboxRequest::new().with_limit(1),
- )
- .await
- .expect("push");
-
- assert_eq!(push_receipt.attempted_events, 1);
- assert_eq!(push_receipt.published_events, 1);
- assert_eq!(push_receipt.retryable_events, 0);
- assert_eq!(push_receipt.terminal_events, 0);
- assert_eq!(push_receipt.events.len(), 1);
- let event = &push_receipt.events[0];
- assert_eq!(event.event_id, enqueue_receipt.signed_event_id);
- assert_eq!(event.outbox_event_id, enqueue_receipt.outbox_event_id);
- assert_eq!(event.final_state, PushOutboxEventState::Published);
- assert_eq!(event.attempted_count, 1);
- assert_eq!(event.accepted_count, 1);
- assert_eq!(event.retryable_count, 0);
- assert_eq!(event.terminal_count, 0);
- assert_eq!(event.quorum, 1);
- assert!(event.quorum_met);
- assert_eq!(event.targets.len(), 1);
- assert_eq!(event.targets[0].endpoint_uri, RELAY);
- assert_eq!(
- event.targets[0].outcome_kind,
- PushOutboxTargetOutcomeKind::Accepted
- );
- assert_eq!(adapter.captured_raw_events().len(), 1);
-
- let outbox = RadrootsOutbox::open_file(&sdk.storage_paths().expect("paths").runtime_path)
- .await
- .expect("outbox");
- let stored = outbox
- .get_event(enqueue_receipt.outbox_event_id)
- .await
- .expect("stored")
- .expect("stored");
- assert_eq!(stored.state, RadrootsOutboxEventState::Published);
-}
-
-#[tokio::test]
-async fn farm_multi_target_profile_publishes_after_nostr_success_and_retains_reticulum() {
- let (_tempdir, sdk) = multi_target_directory_sdk().await;
- let enqueue_receipt = sdk
- .farms()
- .enqueue_publish_with_explicit_signer(
- FarmEnqueuePublishRequest::new(
- farmer_actor(),
- farm(FARM_E_D_TAG, "MultiTarget Farm"),
- TargetPolicy::default_profile(),
- )
- .try_with_idempotency_key("01890f0e-6c00-7000-8000-00000000022f")
- .expect("idempotency key"),
- &FixtureSigner::new(farmer_pubkey()),
- )
- .await
- .expect("enqueue");
- let adapter = RadrootsMockRelayPublishAdapter::new();
-
- let push_receipt = sdk
- .sync()
- .push_outbox_with_transport(
- &RadrootsNostrTransport::new(&adapter),
- PushOutboxRequest::new().with_limit(1),
- )
- .await
- .expect("push");
-
- assert_eq!(push_receipt.attempted_events, 1);
- assert_eq!(push_receipt.published_events, 1);
- assert_eq!(push_receipt.retryable_events, 0);
- assert_eq!(push_receipt.terminal_events, 0);
- let event = &push_receipt.events[0];
- assert_eq!(event.outbox_event_id, enqueue_receipt.outbox_event_id);
- assert_eq!(event.final_state, PushOutboxEventState::Published);
- assert_eq!(event.quorum, 1);
- assert!(event.quorum_met);
- assert_eq!(event.targets.len(), 1);
- assert_eq!(event.targets[0].endpoint_uri, RELAY);
- assert_eq!(
- event.targets[0].outcome_kind,
- PushOutboxTargetOutcomeKind::Accepted
- );
- assert_eq!(adapter.captured_raw_events().len(), 1);
-
- let outbox = RadrootsOutbox::open_file(&sdk.storage_paths().expect("paths").runtime_path)
- .await
- .expect("outbox");
- let stored = outbox
- .get_event(enqueue_receipt.outbox_event_id)
- .await
- .expect("stored")
- .expect("stored");
- assert_eq!(stored.state, RadrootsOutboxEventState::Published);
- let plans = outbox
- .delivery_plans(enqueue_receipt.outbox_event_id)
- .await
- .expect("delivery plans");
- assert_eq!(plans.len(), 1);
- assert_eq!(plans[0].required_success_count, 1);
- assert_eq!(plans[0].status, RadrootsOutboxDeliveryPlanStatus::Complete);
- let targets = outbox
- .delivery_targets(enqueue_receipt.outbox_event_id)
- .await
- .expect("delivery targets");
- assert_eq!(targets.len(), 2);
- assert!(targets.iter().any(|target| {
- target.endpoint_uri.to_string() == RELAY
- && target.status == RadrootsOutboxDeliveryTargetStatus::Accepted
- }));
- assert!(targets.iter().any(|target| {
- target.endpoint_uri.to_string() == "reticulum:local"
- && target.status == RadrootsOutboxDeliveryTargetStatus::DeferredUntilImplemented
- && target.attempt_count == 0
- }));
-}
-
-#[tokio::test]
-async fn farm_enqueue_publish_reports_preflight_idempotency_conflict_without_mutation() {
- let (_tempdir, sdk) = directory_sdk().await;
- let first = FarmEnqueuePublishRequest::new(
- farmer_actor(),
- farm(FARM_E_D_TAG, "North Farm"),
- TargetPolicy::default_profile(),
- )
- .try_with_idempotency_key("01890f0e-6c00-7000-8000-00000000022a")
- .expect("idempotency key");
- sdk.farms()
- .enqueue_publish_with_explicit_signer(first, &FixtureSigner::new(farmer_pubkey()))
- .await
- .expect("first enqueue");
- let paths = sdk.storage_paths().expect("paths");
- let event_store = RadrootsEventStore::open_file(&paths.runtime_path)
- .await
- .expect("event store");
- let outbox = RadrootsOutbox::open_file(&paths.runtime_path)
- .await
- .expect("outbox");
- assert_eq!(
- event_store
- .status_summary()
- .await
- .expect("event store status")
- .total_events,
- 1
- );
- assert_eq!(
- outbox
- .status_summary(0)
- .await
- .expect("outbox status")
- .total_events,
- 1
- );
-
- let second = FarmEnqueuePublishRequest::new(
- farmer_actor(),
- farm(FARM_F_D_TAG, "Changed Farm"),
- TargetPolicy::default_profile(),
- )
- .try_with_idempotency_key("01890f0e-6c00-7000-8000-00000000022a")
- .expect("idempotency key");
- let error = sdk
- .farms()
- .enqueue_publish_with_explicit_signer(second, &FixtureSigner::new(farmer_pubkey()))
- .await
- .expect_err("conflict");
-
- assert!(matches!(
- error,
- RadrootsSdkError::IdempotencyConflict { ref operation_kind, .. }
- if operation_kind == FARM_PUBLISH_OPERATION_KIND
- ));
- assert_eq!(
- error.recovery_actions(),
- vec![RadrootsSdkRecoveryAction::RetryOperationWithSameIdempotencyKey]
- );
- assert!(
- !error
- .to_string()
- .contains("01890f0e-6c00-7000-8000-00000000022a")
- );
- assert_eq!(
- event_store
- .status_summary()
- .await
- .expect("event store status after conflict")
- .total_events,
- 1
- );
- assert_eq!(
- outbox
- .status_summary(0)
- .await
- .expect("outbox status after conflict")
- .total_events,
- 1
- );
-}
-
-#[tokio::test]
-async fn farm_runtime_dtos_serialize_deterministically() {
- let (_tempdir, sdk) = directory_sdk().await;
- let created_at = RadrootsSdkTimestamp::from_unix_seconds(1_700_000_123);
- let prepare_request =
- FarmPreparePublishRequest::new(farmer_actor(), farm(FARM_A_D_TAG, "Serialized Farm"))
- .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,
- serde_json::json!({
- "actor": {
- "pubkey": farmer_pubkey(),
- "roles": ["farmer"],
- "account_id": null,
- "source": "explicit_public_key"
- },
- "farm": {
- "d_tag": FARM_A_D_TAG,
- "name": "Serialized Farm",
- "about": "Vegetable farm",
- "website": "https://example.invalid/north-farm",
- "picture": null,
- "banner": null,
- "location": null,
- "tags": ["vegetables", "local"]
- },
- "created_at": 1_700_000_123
- })
- );
-
- let enqueue_request = FarmEnqueuePublishRequest::new(
- farmer_actor(),
- farm(FARM_B_D_TAG, "Queued Farm"),
- TargetPolicy::default_profile(),
- )
- .try_with_nostr_targets([RELAY, RELAY_B], NostrRelayUrlPolicy::Public)
- .expect("relay targets")
- .with_idempotency_key(
- SdkIdempotencyKey::new("01890f0e-6c00-7000-8000-00000000022b").expect("idempotency"),
- )
- .with_created_at(created_at);
- let enqueue_json = serde_json::to_value(&enqueue_request).expect("enqueue request json");
- assert_struct_serialize_error_paths(&enqueue_request, 5);
-
- assert_eq!(
- enqueue_json,
- serde_json::json!({
- "actor": {
- "pubkey": farmer_pubkey(),
- "roles": ["farmer"],
- "account_id": null,
- "source": "explicit_public_key"
- },
- "farm": {
- "d_tag": FARM_B_D_TAG,
- "name": "Queued Farm",
- "about": "Vegetable farm",
- "website": "https://example.invalid/north-farm",
- "picture": null,
- "banner": null,
- "location": null,
- "tags": ["vegetables", "local"]
- },
- "target_policy": {
- "kind": "explicit",
- "targets": [
- {
- "kind": "nostr",
- "uri": RELAY,
- "scope": null,
- "label": null,
- "fingerprint": "a1997ec4596596af6ffc65e6a30ab7cffa53ea71f524c1c86d64018b96d130af"
- },
- {
- "kind": "nostr",
- "uri": RELAY_B,
- "scope": null,
- "label": null,
- "fingerprint": "5136077cfe7eddcbfaddc5d7bf1f42cdbb8191f3691b86ccc3a81047851cef05"
- }
- ],
- "canonical_targets": [
- "5136077cfe7eddcbfaddc5d7bf1f42cdbb8191f3691b86ccc3a81047851cef05",
- "a1997ec4596596af6ffc65e6a30ab7cffa53ea71f524c1c86d64018b96d130af"
- ]
- },
- "idempotency_key": { "value": "<redacted>", "len": 36 },
- "created_at": 1_700_000_123
- })
- );
- assert!(
- !enqueue_json
- .to_string()
- .contains("01890f0e-6c00-7000-8000-00000000022b")
- );
-
- let try_key_request = FarmEnqueuePublishRequest::new(
- farmer_actor(),
- farm(FARM_C_D_TAG, "Queued Farm"),
- TargetPolicy::default_profile(),
- )
- .try_with_idempotency_key("01890f0e-6c00-7000-8000-00000000022c")
- .expect("try idempotency key");
- assert_eq!(
- serde_json::to_value(&try_key_request).expect("try key request json")["idempotency_key"],
- serde_json::json!({ "value": "<redacted>", "len": 36 })
- );
-
- let private_upsert = FarmPrivateLocationUpsertRequest::new(
- farmer_actor(),
- FARM_C_D_TAG,
- SdkExactLocation::new(48.4359, -123.35155),
- )
- .with_label("north gate")
- .with_updated_at(created_at);
- assert_eq!(
- serde_json::to_value(&private_upsert).expect("private upsert json"),
- serde_json::json!({
- "actor": {
- "pubkey": farmer_pubkey(),
- "roles": ["farmer"],
- "account_id": null,
- "source": "explicit_public_key"
- },
- "farm_d_tag": FARM_C_D_TAG,
- "exact_location": {
- "latitude": 48.4359,
- "longitude": -123.35155
- },
- "label": "north gate",
- "updated_at": 1_700_000_123
- })
- );
- assert_struct_serialize_error_paths(&private_upsert, 5);
-
- let private_set = FarmPrivateLocationSetRequest::exact(
- farmer_actor(),
- FARM_D_D_TAG,
- SdkExactLocation::new(48.9, -123.4),
- )
- .with_label("identifier gate")
- .with_updated_at(created_at);
- assert_eq!(
- serde_json::to_value(&private_set).expect("private set json"),
- serde_json::json!({
- "actor": {
- "pubkey": farmer_pubkey(),
- "roles": ["farmer"],
- "account_id": null,
- "source": "explicit_public_key"
- },
- "farm_d_tag": FARM_D_D_TAG,
- "input": {
- "kind": "exact",
- "value": {
- "latitude": 48.9,
- "longitude": -123.4
- }
- },
- "label": "identifier gate",
- "updated_at": 1_700_000_123
- })
- );
- assert_struct_serialize_error_paths(&private_set, 5);
-
- assert_eq!(
- serde_json::to_value(FarmPrivateLocationSetRequest::query(
- farmer_actor(),
- FARM_D_D_TAG,
- "Shared Market, BC, CA"
- ))
- .expect("private query set json")["input"],
- serde_json::json!({
- "kind": "locality",
- "value": {
- "input": {
- "Query": "Shared Market, BC, CA"
- },
- "limit": 10
- }
- })
- );
- assert_eq!(
- serde_json::to_value(FarmPrivateLocationSetRequest::geonames_id(
- farmer_actor(),
- FARM_D_D_TAG,
- 3004
- ))
- .expect("private geonames id set json")["input"],
- serde_json::json!({
- "kind": "locality",
- "value": {
- "input": {
- "FeatureId": 3004
- },
- "limit": 10
- }
- })
- );
-
- let private_clear = FarmPrivateLocationClearRequest::new(farmer_actor(), FARM_E_D_TAG);
- assert_eq!(
- serde_json::to_value(&private_clear).expect("private clear json"),
- serde_json::json!({
- "actor": {
- "pubkey": farmer_pubkey(),
- "roles": ["farmer"],
- "account_id": null,
- "source": "explicit_public_key"
- },
- "farm_d_tag": FARM_E_D_TAG
- })
- );
- assert_struct_serialize_error_paths(&private_clear, 2);
-
- let private_receipt = FarmPrivateLocationReceipt {
- farm_addr: farm_addr(&farmer_actor(), FARM_D_D_TAG),
- 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),
- public_locality: SdkPublicLocality {
- primary: "Identifier Grove, British Columbia, Canada".to_owned(),
- city: Some("Identifier Grove".to_owned()),
- region: Some("British Columbia".to_owned()),
- country: Some("Canada".to_owned()),
- geohash5: "c28rn".to_owned(),
- },
- geonames_feature_id: Some(3004),
- geonames_country_id: Some("CA".to_owned()),
- updated_at_ms: 1_700_000_123_000,
- };
- let private_receipt_json =
- serde_json::to_value(&private_receipt).expect("private receipt json");
- assert_struct_serialize_error_paths(&private_receipt, 9);
- assert_struct_serialize_error_paths(&private_receipt.exact_location, 2);
- assert_struct_serialize_error_paths(&private_receipt.public_locality, 5);
- assert_eq!(private_receipt_json["updated_at_ms"], 1_700_000_123_000_i64);
- let listing_location = private_receipt.public_locality.to_listing_public_location();
- assert_eq!(
- listing_location.primary,
- "Identifier Grove, British Columbia, Canada"
- );
- assert_eq!(listing_location.city.as_deref(), Some("Identifier Grove"));
- assert_eq!(listing_location.region.as_deref(), Some("British Columbia"));
- assert_eq!(listing_location.country.as_deref(), Some("Canada"));
- assert_eq!(listing_location.geohash, "c28rn");
- let farm_location = private_receipt.public_locality.to_farm_public_location();
- assert_eq!(
- farm_location.primary,
- "Identifier Grove, British Columbia, Canada"
- );
- assert_eq!(farm_location.city.as_deref(), Some("Identifier Grove"));
- assert_eq!(farm_location.region.as_deref(), Some("British Columbia"));
- assert_eq!(farm_location.country.as_deref(), Some("Canada"));
- assert_eq!(farm_location.geohash, "c28rn");
- assert_eq!(
- serde_json::from_value::<FarmPrivateLocationReceipt>(private_receipt_json)
- .expect("private receipt round trip"),
- private_receipt
- );
-
- let candidate = FarmPrivateLocationLookupCandidate {
- geonames_feature_id: 3002,
- geonames_country_id: "CA".to_owned(),
- name: "Shared Market".to_owned(),
- display_name: "Shared Market, British Columbia, Canada".to_owned(),
- exact_location: SdkExactLocation::new(48.7, -123.2),
- region: Some("British Columbia".to_owned()),
- country: Some("Canada".to_owned()),
- };
- assert_struct_serialize_error_paths(&candidate, 7);
- let lookup = FarmPrivateLocationLookupReceipt {
- farm_addr: farm_addr(&farmer_actor(), FARM_F_D_TAG),
- farm_pubkey: farmer_pubkey().to_owned(),
- farm_d_tag: FARM_F_D_TAG.to_owned(),
- input: FarmPrivateLocationInput::query("Shared Market"),
- candidates: vec![candidate],
- };
- let lookup_json = serde_json::to_value(&lookup).expect("lookup receipt json");
- assert_struct_serialize_error_paths(&lookup, 5);
- assert_eq!(lookup_json["candidates"][0]["geonames_feature_id"], 3002);
- assert_eq!(
- serde_json::from_value::<FarmPrivateLocationLookupReceipt>(lookup_json)
- .expect("lookup receipt round trip"),
- lookup
- );
- for result in [
- FarmPrivateLocationSetResult::Stored(private_receipt),
- FarmPrivateLocationSetResult::Ambiguous(lookup.clone()),
- FarmPrivateLocationSetResult::NoMatch(FarmPrivateLocationLookupReceipt {
- candidates: Vec::new(),
- ..lookup
- }),
- ] {
- let value = serde_json::to_value(&result).expect("location set result json");
- let round_trip = serde_json::from_value::<FarmPrivateLocationSetResult>(value)
- .expect("location set result round trip");
- assert_eq!(round_trip, result);
- }
-
- let receipt = sdk
- .farms()
- .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");
-
- assert_eq!(
- receipt_json,
- serde_json::json!({
- "farm_addr": receipt.farm_addr.as_str(),
- "expected_event_id": receipt.expected_event_id.to_hex(),
- "signed_event_id": receipt.signed_event_id.to_hex(),
- "local_event_seq": 1,
- "outbox_operation_id": 1,
- "outbox_event_id": 1,
- "state": "stored_and_queued",
- "idempotency_digest_prefix": receipt.idempotency_digest_prefix.as_deref()
- })
- );
-}
diff --git a/crates/sdk/tests/package_boundary.rs b/crates/sdk/tests/package_boundary.rs
@@ -3,6 +3,7 @@ use std::collections::BTreeSet;
const MANIFEST: &str = include_str!("../Cargo.toml");
const ROOT: &str = include_str!("../src/lib.rs");
const CLIENT: &str = include_str!("../src/client.rs");
+const FARM: &str = include_str!("../src/farm.rs");
const SYNC: &str = include_str!("../src/sync.rs");
const TRANSPORT: &str = include_str!("../src/transport.rs");
@@ -195,6 +196,40 @@ fn sync_operations_only_delegate_to_the_canonical_engine() {
);
}
+#[test]
+fn farm_operations_preserve_pure_planning_commit_and_privacy_boundaries() {
+ for required in [
+ "encode::farm::to_wire_parts",
+ "AddressableCoordinate",
+ "EventDraft",
+ "radroots_sync::PushRequest::new",
+ "self.sync",
+ ".sign_and_enqueue(",
+ "PrivateArtifactStore",
+ ] {
+ assert!(
+ FARM.contains(required),
+ "missing farm boundary `{required}`"
+ );
+ }
+ for forbidden in [
+ "SdkExactLocation",
+ "SdkPublicLocality",
+ "latitude:",
+ "longitude:",
+ "enqueue_signed_workflow",
+ "local_event_seq",
+ "outbox_event_id",
+ ] {
+ assert!(
+ !FARM.contains(forbidden),
+ "farm source contains retired SDK or private representation `{forbidden}`"
+ );
+ }
+ let source_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
+ assert!(!source_root.join("farms_runtime.rs").exists());
+}
+
fn dependency_names(manifest: &str) -> BTreeSet<&str> {
let dependencies = manifest
.split_once("[dependencies]")
diff --git a/crates/sdk/tests/unit/farms_runtime_tests.rs b/crates/sdk/tests/unit/farms_runtime_tests.rs
@@ -1,1090 +0,0 @@
-use super::*;
-use crate::{RadrootsSdkLocalKeySigner, RadrootsSdkSignerProvider};
-use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
-
-use crate::fixture_signer::{FixtureSigner, fixture_alice_pubkey, fixture_bob_pubkey};
-use crate::serializer_failure::assert_struct_serialize_error_paths;
-use radroots_signing::actor::ActorSource;
-
-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() -> Actor {
- Actor::from_public_key_hex(
- farmer_pubkey(),
- ActorSource::ExplicitPublicKey,
- [AuthorRole::Farmer],
- )
- .expect("actor")
-}
-
-fn farm(d_tag: &str, name: &str) -> Farm {
- Farm {
- d_tag: d_tag.to_owned(),
- name: name.to_owned(),
- about: Some("Vegetable farm".to_owned()),
- website: Some("https://example.invalid/farm".to_owned()),
- picture: None,
- banner: None,
- location: None,
- tags: Some(vec!["vegetables".to_owned(), "local".to_owned()]),
- }
-}
-
-async fn fixture_geocoder(tempdir: &tempfile::TempDir, feature_name: Option<&str>) -> Geocoder {
- let path = tempdir.path().join(match feature_name {
- Some(name) if name.trim().is_empty() => "geonames-blank.db",
- Some(_) => "geonames-fixture.db",
- None => "geonames-empty.db",
- });
- let options = SqliteConnectOptions::new()
- .filename(&path)
- .create_if_missing(true);
- let pool = SqlitePoolOptions::new()
- .max_connections(1)
- .connect_with(options)
- .await
- .expect("geonames fixture pool");
- sqlx::raw_sql(
- r#"
- CREATE TABLE countries(
- id TEXT,
- name TEXT,
- PRIMARY KEY (id)
- );
- CREATE TABLE admin1(
- country_id TEXT,
- id INTEGER,
- name TEXT,
- PRIMARY KEY (country_id, id)
- );
- CREATE TABLE features(
- id INTEGER,
- name TEXT,
- country_id TEXT,
- admin1_id INTEGER,
- PRIMARY KEY (id)
- );
- CREATE TABLE coordinates(
- feature_id INTEGER,
- latitude REAL,
- longitude REAL,
- PRIMARY KEY (feature_id)
- );
- CREATE INDEX coordinates_lat_lng ON coordinates (latitude, longitude);
- CREATE VIEW geonames AS
- SELECT
- features.id,
- features.name,
- admin1.id AS admin1_id,
- admin1.name AS admin1_name,
- countries.id AS country_id,
- countries.name AS country_name,
- coordinates.latitude AS latitude,
- coordinates.longitude AS longitude
- FROM features
- LEFT JOIN countries ON features.country_id = countries.id
- LEFT JOIN admin1 ON features.country_id = admin1.country_id AND features.admin1_id = admin1.id
- JOIN coordinates ON features.id = coordinates.feature_id;
- "#,
- )
- .execute(&pool)
- .await
- .expect("schema geonames fixture");
- if let Some(name) = feature_name {
- sqlx::raw_sql(
- r#"
- INSERT INTO countries (id, name) VALUES ('FX', 'Fixture Country');
- INSERT INTO admin1 (country_id, id, name) VALUES ('FX', 1, 'Fixture Region');
- INSERT INTO coordinates (feature_id, latitude, longitude) VALUES (1, 12.25, -34.50);
- "#,
- )
- .execute(&pool)
- .await
- .expect("seed geonames fixture");
- sqlx::query(
- "INSERT INTO features (id, name, country_id, admin1_id) VALUES (1, ?1, 'FX', 1)",
- )
- .bind(name)
- .execute(&pool)
- .await
- .expect("seed geonames feature");
- }
- pool.close().await;
- Geocoder::open_path(path).expect("open geonames fixture")
-}
-
-#[test]
-fn farm_publish_plan_rejects_invalid_draft_tags() {
- let actor = Actor::from_public_key_hex(
- "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
- ActorSource::ExplicitPublicKey,
- [AuthorRole::Farmer],
- )
- .expect("actor");
- let farm = Farm {
- d_tag: "AAAAAAAAAAAAAAAAAAAAA!".to_owned(),
- name: "Invalid Farm".to_owned(),
- about: None,
- website: None,
- picture: None,
- banner: None,
- location: None,
- tags: None,
- };
- let error = farm_publish_plan(
- &actor,
- farm,
- RadrootsSdkTimestamp::from_unix_seconds(1_700_000_000),
- )
- .expect_err("invalid farm plan");
- assert!(matches!(
- error,
- RadrootsSdkError::InvalidRequest { message } if message.contains("draft encode failed")
- ));
-
- assert!(matches!(
- farm_addr(&actor, ""),
- Err(RadrootsSdkError::InvalidRequest { message }) if message.contains("farm address")
- ));
-}
-
-#[test]
-fn farm_runtime_request_builders_and_serializers_cover_success_paths() {
- let created_at = RadrootsSdkTimestamp::from_unix_seconds(1_700_000_321);
- let prepare =
- FarmPreparePublishRequest::new(farmer_actor(), farm(FARM_A_D_TAG, "Serialized Farm"))
- .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_pubkey());
- assert_eq!(prepare_json["created_at"], 1_700_000_321);
-
- let enqueue = FarmEnqueuePublishRequest::new(
- farmer_actor(),
- farm(FARM_B_D_TAG, "Queued Farm"),
- TargetPolicy::default_profile(),
- )
- .try_with_nostr_targets([RELAY_A, RELAY_B], NostrRelayUrlPolicy::Public)
- .expect("relay targets")
- .with_idempotency_key(
- SdkIdempotencyKey::new("01890f0e-6c00-7000-8000-000000000233").expect("key"),
- )
- .with_created_at(created_at);
- assert_struct_serialize_error_paths(&enqueue, 5);
- let enqueue_json = serde_json::to_value(&enqueue).expect("enqueue json");
- assert_eq!(enqueue_json["target_policy"]["kind"], "explicit");
- assert_eq!(enqueue_json["created_at"], 1_700_000_321);
- assert!(
- !enqueue_json
- .to_string()
- .contains("01890f0e-6c00-7000-8000-000000000233")
- );
-
- let try_key = FarmEnqueuePublishRequest::new(
- farmer_actor(),
- farm(FARM_C_D_TAG, "Try Key Farm"),
- TargetPolicy::default_profile(),
- )
- .try_with_idempotency_key("01890f0e-6c00-7000-8000-000000000234")
- .expect("try key");
- assert_eq!(
- serde_json::to_value(&try_key).expect("try key json")["idempotency_key"]["len"],
- "01890f0e-6c00-7000-8000-000000000234".len()
- );
-
- let private_upsert = FarmPrivateLocationUpsertRequest::new(
- farmer_actor(),
- FARM_A_D_TAG,
- SdkExactLocation::new(12.25, -34.5),
- )
- .with_label("unit gate")
- .with_updated_at(created_at);
- assert_struct_serialize_error_paths(&private_upsert, 5);
- let private_upsert_json = serde_json::to_value(&private_upsert).expect("private upsert json");
- assert_eq!(private_upsert_json["label"], "unit gate");
- assert_eq!(private_upsert_json["updated_at"], 1_700_000_321);
-
- for input in [
- FarmPrivateLocationInput::exact(SdkExactLocation::new(12.25, -34.5)),
- FarmPrivateLocationInput::city("Fixture City"),
- FarmPrivateLocationInput::query("Fixture City, FX"),
- FarmPrivateLocationInput::geonames_id(42),
- ] {
- let input_json = serde_json::to_value(&input).expect("private input json");
- assert_eq!(
- serde_json::from_value::<FarmPrivateLocationInput>(input_json)
- .expect("private input round trip"),
- input
- );
- }
-
- let private_set =
- FarmPrivateLocationSetRequest::query(farmer_actor(), FARM_B_D_TAG, "Fixture City, FX")
- .with_label("query gate")
- .with_updated_at(created_at);
- assert_struct_serialize_error_paths(&private_set, 5);
- let private_set_json = serde_json::to_value(&private_set).expect("private set json");
- assert_eq!(private_set_json["label"], "query gate");
- assert_eq!(private_set_json["input"]["kind"], "locality");
-
- let private_exact = FarmPrivateLocationSetRequest::exact(
- farmer_actor(),
- FARM_B_D_TAG,
- SdkExactLocation::new(12.25, -34.5),
- );
- assert_eq!(
- serde_json::to_value(&private_exact).expect("private exact set json")["input"]["kind"],
- "exact"
- );
-
- let private_city =
- FarmPrivateLocationSetRequest::city(farmer_actor(), FARM_B_D_TAG, "Fixture City");
- let private_city_json = serde_json::to_value(&private_city).expect("private city set json");
- assert_eq!(private_city_json["input"]["kind"], "locality");
- assert_eq!(
- private_city_json["input"]["value"]["input"]["Structured"]["locality"],
- "Fixture City"
- );
-
- let private_geonames =
- FarmPrivateLocationSetRequest::geonames_id(farmer_actor(), FARM_B_D_TAG, 42);
- assert_eq!(
- serde_json::to_value(&private_geonames).expect("private geonames set json")["input"]["value"]
- ["input"]["FeatureId"],
- 42
- );
-
- let private_clear = FarmPrivateLocationClearRequest::new(farmer_actor(), FARM_C_D_TAG);
- assert_struct_serialize_error_paths(&private_clear, 2);
- assert_eq!(
- serde_json::to_value(&private_clear).expect("private clear json")["farm_d_tag"],
- FARM_C_D_TAG
- );
-
- let public_locality = SdkPublicLocality {
- primary: "Fixture City, Fixture Region, Fixture Country".to_owned(),
- city: Some("Fixture City".to_owned()),
- region: Some("Fixture Region".to_owned()),
- country: Some("Fixture Country".to_owned()),
- geohash5: "e4pmw".to_owned(),
- };
- assert_struct_serialize_error_paths(&public_locality, 5);
- assert_eq!(
- public_locality.to_listing_public_location().geohash,
- "e4pmw"
- );
- assert_eq!(public_locality.to_farm_public_location().geohash, "e4pmw");
-
- let private_receipt = FarmPrivateLocationReceipt {
- farm_addr: farm_addr(&farmer_actor(), FARM_B_D_TAG).expect("farm addr"),
- 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),
- public_locality,
- geonames_feature_id: Some(42),
- geonames_country_id: Some("FX".to_owned()),
- updated_at_ms: 1_700_000_321_000,
- };
- assert_struct_serialize_error_paths(&private_receipt, 9);
- assert_struct_serialize_error_paths(&private_receipt.exact_location, 2);
- let private_receipt_json =
- serde_json::to_value(&private_receipt).expect("private receipt json");
- assert_eq!(private_receipt_json["updated_at_ms"], 1_700_000_321_000_i64);
- assert_eq!(
- serde_json::from_value::<FarmPrivateLocationReceipt>(private_receipt_json)
- .expect("private receipt round trip"),
- private_receipt
- );
-
- let clear_receipt = FarmPrivateLocationClearReceipt {
- farm_addr: farm_addr(&farmer_actor(), FARM_C_D_TAG).expect("farm addr"),
- cleared: true,
- };
- assert_struct_serialize_error_paths(&clear_receipt, 2);
- assert_eq!(
- serde_json::from_value::<FarmPrivateLocationClearReceipt>(
- serde_json::to_value(&clear_receipt).expect("clear receipt json")
- )
- .expect("clear receipt round trip"),
- clear_receipt
- );
-
- let candidate = FarmPrivateLocationLookupCandidate {
- geonames_feature_id: 42,
- geonames_country_id: "FX".to_owned(),
- name: "Fixture City".to_owned(),
- display_name: "Fixture City, Fixture Region, Fixture Country".to_owned(),
- exact_location: SdkExactLocation::new(12.25, -34.5),
- region: Some("Fixture Region".to_owned()),
- country: Some("Fixture Country".to_owned()),
- };
- 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_pubkey().to_owned(),
- farm_d_tag: FARM_B_D_TAG.to_owned(),
- input: FarmPrivateLocationInput::query("Fixture City, FX"),
- candidates: vec![candidate],
- };
- assert_struct_serialize_error_paths(&lookup, 5);
- assert_eq!(
- serde_json::from_value::<FarmPrivateLocationLookupReceipt>(
- serde_json::to_value(&lookup).expect("lookup json")
- )
- .expect("lookup round trip"),
- lookup
- );
-
- for result in [
- FarmPrivateLocationSetResult::Stored(private_receipt),
- FarmPrivateLocationSetResult::Ambiguous(lookup.clone()),
- FarmPrivateLocationSetResult::NoMatch(FarmPrivateLocationLookupReceipt {
- candidates: Vec::new(),
- ..lookup
- }),
- ] {
- let result_json = serde_json::to_value(&result).expect("location result json");
- assert_eq!(
- serde_json::from_value::<FarmPrivateLocationSetResult>(result_json)
- .expect("location result round trip"),
- result
- );
- }
-}
-
-#[test]
-fn farm_request_builders_reject_invalid_options_and_timestamp_bounds() {
- let invalid_relays = FarmEnqueuePublishRequest::new(
- farmer_actor(),
- farm(FARM_A_D_TAG, "Invalid Relay Farm"),
- TargetPolicy::default_profile(),
- )
- .try_with_nostr_targets(["http://relay.radroots.test"], NostrRelayUrlPolicy::Public);
- assert!(invalid_relays.is_err());
-
- let invalid_key = FarmEnqueuePublishRequest::new(
- farmer_actor(),
- farm(FARM_B_D_TAG, "Invalid Key Farm"),
- TargetPolicy::default_profile(),
- )
- .try_with_idempotency_key("");
- assert!(invalid_key.is_err());
-
- let timestamp_error = farm_publish_plan(
- &farmer_actor(),
- farm(FARM_C_D_TAG, "Future Farm"),
- RadrootsSdkTimestamp::from_unix_seconds(u64::MAX),
- )
- .expect_err("timestamp error");
- assert!(matches!(
- timestamp_error,
- RadrootsSdkError::TimestampOutOfRange { .. }
- ));
-}
-
-#[test]
-fn farm_public_locality_derivation_covers_country_fallback_and_empty_names() {
- let reverse = GeocoderReverseResult {
- id: 1,
- name: " Fixture Town ".to_owned(),
- admin1_id: None,
- admin1_name: None,
- country_id: "FX".to_owned(),
- country_name: None,
- latitude: 12.25,
- longitude: -34.50,
- };
- let locality = public_locality_from_reverse(SdkExactLocation::new(12.26, -34.51), &reverse)
- .expect("locality");
- assert_eq!(locality.primary, "Fixture Town");
- assert_eq!(locality.city.as_deref(), Some("Fixture Town"));
- assert_eq!(locality.region, None);
- assert_eq!(locality.country.as_deref(), Some("FX"));
- assert_eq!(locality.geohash5, "e4pmw");
-
- let named_region = GeocoderReverseResult {
- admin1_name: Some(" Fixture Region ".to_owned()),
- country_name: Some(" Fixture Country ".to_owned()),
- ..reverse.clone()
- };
- let named_locality =
- public_locality_from_reverse(SdkExactLocation::new(12.26, -34.51), &named_region)
- .expect("named locality");
- assert_eq!(named_locality.region.as_deref(), Some("Fixture Region"));
- assert_eq!(named_locality.country.as_deref(), Some("Fixture Country"));
-
- let blank_optional_names = GeocoderReverseResult {
- admin1_name: Some(" ".to_owned()),
- country_name: Some(" ".to_owned()),
- ..reverse.clone()
- };
- let fallback_locality =
- public_locality_from_reverse(SdkExactLocation::new(12.26, -34.51), &blank_optional_names)
- .expect("fallback locality");
- assert_eq!(fallback_locality.region, None);
- assert_eq!(fallback_locality.country.as_deref(), Some("FX"));
-
- let blank_name = GeocoderReverseResult {
- name: " ".to_owned(),
- ..reverse
- };
- assert!(matches!(
- public_locality_from_reverse(SdkExactLocation::new(12.26, -34.51), &blank_name),
- Err(RadrootsSdkError::GeoNames {
- kind: crate::RadrootsSdkGeoNamesErrorKind::Lookup,
- ..
- })
- ));
-
- for location in [
- SdkExactLocation::new(f64::NAN, -34.51),
- SdkExactLocation::new(12.26, f64::INFINITY),
- SdkExactLocation::new(-90.1, -34.51),
- SdkExactLocation::new(90.1, -34.51),
- SdkExactLocation::new(12.26, -180.1),
- SdkExactLocation::new(12.26, 180.1),
- ] {
- assert!(matches!(
- validate_exact_location(location),
- Err(RadrootsSdkError::InvalidRequest { .. })
- ));
- assert!(matches!(
- geohash5(location),
- Err(RadrootsSdkError::InvalidRequest { .. })
- ));
- }
- assert!(matches!(
- sdk_timestamp_ms(RadrootsSdkTimestamp::from_unix_seconds(u64::MAX)),
- Err(RadrootsSdkError::TimestampOutOfRange { .. })
- ));
- assert!(matches!(
- sdk_timestamp_ms(RadrootsSdkTimestamp::from_unix_seconds(
- (i64::MAX as u64 / 1_000) + 1
- )),
- Err(RadrootsSdkError::TimestampOutOfRange { .. })
- ));
-}
-
-#[tokio::test]
-async fn farm_client_prepare_resolves_default_and_explicit_created_at() {
- let sdk = crate::RadrootsClient::builder()
- .fixed_clock(RadrootsSdkTimestamp::from_unix_seconds(1_700_000_400))
- .build()
- .await
- .expect("sdk");
- let default_plan = sdk
- .farms()
- .prepare_publish(FarmPreparePublishRequest::new(
- farmer_actor(),
- farm(FARM_A_D_TAG, "Default Clock Farm"),
- ))
- .expect("default plan");
- assert_eq!(
- default_plan.created_at(),
- RadrootsSdkTimestamp::from_unix_seconds(1_700_000_400)
- );
-
- let explicit_plan = sdk
- .farms()
- .prepare_publish(
- FarmPreparePublishRequest::new(
- farmer_actor(),
- farm(FARM_B_D_TAG, "Explicit Clock Farm"),
- )
- .with_created_at(RadrootsSdkTimestamp::from_unix_seconds(1_700_000_401)),
- )
- .expect("explicit plan");
- assert_eq!(
- 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 = EventDraft::new(
- "radroots.social.geochat.v1",
- radroots_event::envelope::kind::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 =
- EventId::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 =
- AddressableCoordinate::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)
- .build()
- .await
- .expect("sdk");
- let error = sdk
- .farms()
- .prepare_publish(FarmPreparePublishRequest::new(
- farmer_actor(),
- farm(FARM_A_D_TAG, "Clock Error Farm"),
- ))
- .expect_err("clock error");
- assert!(matches!(error, RadrootsSdkError::ClockBeforeUnixEpoch));
-}
-
-#[tokio::test]
-async fn farm_enqueue_publish_reports_prepare_errors_before_signing() {
- let sdk = crate::RadrootsClient::builder()
- .fixed_clock(RadrootsSdkTimestamp::from_unix_seconds(1_700_000_500))
- .build()
- .await
- .expect("sdk");
- let error = sdk
- .farms()
- .enqueue_publish_with_explicit_signer(
- FarmEnqueuePublishRequest::new(
- farmer_actor(),
- farm("AAAAAAAAAAAAAAAAAAAAA!", "Invalid Enqueue Farm"),
- TargetPolicy::try_nostr_relays([RELAY_A], NostrRelayUrlPolicy::Public)
- .expect("transport targets"),
- ),
- &FixtureSigner::new(farmer_pubkey()),
- )
- .await
- .expect_err("prepare error");
- assert!(matches!(error, RadrootsSdkError::InvalidRequest { .. }));
-}
-
-#[tokio::test]
-async fn farm_client_enqueue_methods_cover_source_attached_workflow_paths() {
- let sdk = crate::RadrootsClient::builder()
- .fixed_clock(RadrootsSdkTimestamp::from_unix_seconds(1_700_000_500))
- .build()
- .await
- .expect("sdk");
- let signer = FixtureSigner::new(farmer_pubkey());
- let actor = farmer_actor();
- let receipt = sdk
- .farms()
- .enqueue_publish_with_explicit_signer(
- FarmEnqueuePublishRequest::new(
- actor.clone(),
- farm(FARM_A_D_TAG, "Enqueued Farm"),
- TargetPolicy::try_nostr_relays([RELAY_A], NostrRelayUrlPolicy::Public)
- .expect("transport targets"),
- )
- .try_with_idempotency_key("01890f0e-6c00-7000-8000-000000000235")
- .expect("idempotency"),
- &signer,
- )
- .await
- .expect("enqueue farm");
- assert_eq!(receipt.signed_event_id, receipt.expected_event_id);
- assert_eq!(receipt.state, SdkMutationState::StoredAndQueued);
-
- let plan = sdk
- .farms()
- .prepare_publish(FarmPreparePublishRequest::new(
- actor.clone(),
- farm(FARM_B_D_TAG, "Prepared Farm"),
- ))
- .expect("prepared farm");
- let prepared = sdk
- .farms()
- .enqueue_prepared_publish_with_explicit_signer(
- &actor,
- plan,
- TargetPolicy::try_nostr_relays([RELAY_B], NostrRelayUrlPolicy::Public)
- .expect("prepared transport targets"),
- Some(
- SdkIdempotencyKey::new("01890f0e-6c00-7000-8000-000000000236")
- .expect("prepared idempotency"),
- ),
- &signer,
- )
- .await
- .expect("enqueue prepared farm");
- assert_eq!(prepared.signed_event_id, prepared.expected_event_id);
- assert_eq!(prepared.local_event_seq, 2);
-}
-
-#[tokio::test]
-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_signer(
- FixtureSigner::new(farmer_pubkey()),
- farmer_pubkey(),
- )
- .expect("signer"),
- ))
- .build()
- .await
- .expect("sdk");
- let actor = Actor::from_public_key_hex(
- farmer_pubkey(),
- ActorSource::ExplicitPublicKey,
- [AuthorRole::Farmer],
- )
- .expect("actor");
-
- let receipt = sdk
- .farms()
- .enqueue_publish(
- FarmEnqueuePublishRequest::new(
- actor,
- farm(FARM_C_D_TAG, "Configured Farm"),
- TargetPolicy::try_nostr_relays([RELAY_A], NostrRelayUrlPolicy::Public)
- .expect("transport targets"),
- )
- .try_with_idempotency_key("01890f0e-6c00-7000-8000-000000000236")
- .expect("idempotency"),
- )
- .await
- .expect("enqueue farm");
-
- assert_eq!(receipt.signed_event_id, receipt.expected_event_id);
- assert_eq!(receipt.state, SdkMutationState::StoredAndQueued);
-}
-
-#[tokio::test]
-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_signer(
- FixtureSigner::new(farmer_pubkey()),
- farmer_pubkey(),
- )
- .expect("signer"),
- ))
- .build()
- .await
- .expect("configured sdk");
- let actor = Actor::from_public_key_hex(
- farmer_pubkey(),
- ActorSource::ExplicitPublicKey,
- [AuthorRole::Farmer],
- )
- .expect("actor");
-
- assert!(matches!(
- configured_sdk
- .farms()
- .enqueue_publish(FarmEnqueuePublishRequest::new(
- actor.clone(),
- farm("AAAAAAAAAAAAAAAAAAAAA!", "Invalid Configured Farm"),
- TargetPolicy::try_nostr_relays([RELAY_A], NostrRelayUrlPolicy::Public)
- .expect("transport targets"),
- ))
- .await,
- Err(RadrootsSdkError::InvalidRequest { .. })
- ));
-
- let no_signer_sdk = crate::RadrootsClient::builder()
- .fixed_clock(RadrootsSdkTimestamp::from_unix_seconds(1_700_000_500))
- .build()
- .await
- .expect("no signer sdk");
- let plan = no_signer_sdk
- .farms()
- .prepare_publish(FarmPreparePublishRequest::new(
- actor.clone(),
- farm(FARM_A_D_TAG, "Missing Configured Signer Farm"),
- ))
- .expect("plan");
- assert!(matches!(
- no_signer_sdk
- .farms()
- .enqueue_prepared_publish(
- &actor,
- plan,
- TargetPolicy::try_nostr_relays([RELAY_A], NostrRelayUrlPolicy::Public)
- .expect("transport targets"),
- Some(
- SdkIdempotencyKey::new("01890f0e-6c00-7000-8000-000000000237")
- .expect("idempotency"),
- ),
- )
- .await,
- Err(RadrootsSdkError::SignerUnavailable { .. })
- ));
-}
-
-#[tokio::test]
-async fn farm_private_location_default_client_and_lookup_report_store_edges() {
- let tempdir = tempfile::tempdir().expect("tempdir");
- let geocoder = fixture_geocoder(&tempdir, Some("Fixture Town")).await;
- let empty_geocoder = fixture_geocoder(&tempdir, None).await;
- let blank_geocoder = fixture_geocoder(&tempdir, Some(" ")).await;
- let sdk = crate::RadrootsClient::builder()
- .fixed_clock(RadrootsSdkTimestamp::from_unix_seconds(1_700_000_500))
- .build()
- .await
- .expect("sdk");
- let actor = farmer_actor();
- let request = FarmPrivateLocationUpsertRequest::new(
- actor.clone(),
- FARM_A_D_TAG,
- SdkExactLocation::new(12.26, -34.51),
- );
-
- assert!(matches!(
- sdk.farms().upsert_private_location(request).await,
- Err(RadrootsSdkError::GeoNames {
- kind: crate::RadrootsSdkGeoNamesErrorKind::Configuration,
- ..
- })
- ));
- assert!(matches!(
- sdk.farms()
- .set_private_location(FarmPrivateLocationSetRequest::city(
- actor.clone(),
- FARM_A_D_TAG,
- "Fixture Town",
- ))
- .await,
- Err(RadrootsSdkError::GeoNames {
- kind: crate::RadrootsSdkGeoNamesErrorKind::Configuration,
- ..
- })
- ));
-
- let farm_a_addr = farm_addr(&actor, FARM_A_D_TAG).expect("farm addr");
- assert_eq!(
- sdk.farms()
- .private_location(&farm_a_addr)
- .await
- .expect("missing location"),
- None
- );
-
- let stored = sdk
- .farms()
- .upsert_private_location_with_geocoder(
- FarmPrivateLocationUpsertRequest::new(
- actor.clone(),
- FARM_A_D_TAG,
- SdkExactLocation::new(12.26, -34.51),
- )
- .with_updated_at(RadrootsSdkTimestamp::from_unix_seconds(1_700_000_501)),
- &geocoder,
- )
- .await
- .expect("stored location");
- assert_eq!(stored.farm_addr, farm_a_addr);
- assert_eq!(stored.public_locality.primary, "Fixture Town");
- assert_eq!(
- sdk.farms()
- .private_location(&farm_a_addr)
- .await
- .expect("stored lookup")
- .expect("stored location")
- .updated_at_ms,
- 1_700_000_501_000
- );
-
- let missing_clear = sdk
- .farms()
- .clear_private_location(FarmPrivateLocationClearRequest::new(
- actor.clone(),
- FARM_B_D_TAG,
- ))
- .await
- .expect("clear missing location");
- assert!(!missing_clear.cleared);
- assert_eq!(
- missing_clear.farm_addr,
- farm_addr(&actor, FARM_B_D_TAG).expect("farm b addr")
- );
-
- let non_farmer_actor = Actor::from_public_key_hex(
- farmer_pubkey(),
- ActorSource::ExplicitPublicKey,
- [AuthorRole::Buyer],
- )
- .expect("buyer actor");
- assert!(matches!(
- sdk.farms()
- .clear_private_location(FarmPrivateLocationClearRequest::new(
- non_farmer_actor,
- FARM_A_D_TAG,
- ))
- .await,
- Err(RadrootsSdkError::UnauthorizedActor { .. })
- ));
- assert!(matches!(
- sdk.farms()
- .clear_private_location(FarmPrivateLocationClearRequest::new(
- actor.clone(),
- "bad d tag",
- ))
- .await,
- Err(RadrootsSdkError::InvalidRequest { .. })
- ));
-
- let cleared = sdk
- .farms()
- .clear_private_location(FarmPrivateLocationClearRequest::new(
- actor.clone(),
- FARM_A_D_TAG,
- ))
- .await
- .expect("clear stored location");
- assert!(cleared.cleared);
- assert_eq!(cleared.farm_addr, farm_a_addr);
- assert_eq!(
- sdk.farms()
- .private_location(&farm_a_addr)
- .await
- .expect("location cleared"),
- None
- );
-
- assert!(matches!(
- sdk.farms()
- .upsert_private_location_with_geocoder(
- FarmPrivateLocationUpsertRequest::new(
- actor.clone(),
- FARM_B_D_TAG,
- SdkExactLocation::new(12.26, -34.51),
- )
- .with_updated_at(RadrootsSdkTimestamp::from_unix_seconds(u64::MAX)),
- &geocoder,
- )
- .await,
- Err(RadrootsSdkError::TimestampOutOfRange { .. })
- ));
- assert!(matches!(
- sdk.farms()
- .upsert_private_location_with_geocoder(
- FarmPrivateLocationUpsertRequest::new(
- actor.clone(),
- FARM_B_D_TAG,
- SdkExactLocation::new(12.26, -34.51),
- ),
- &empty_geocoder,
- )
- .await,
- Err(RadrootsSdkError::GeoNames {
- kind: crate::RadrootsSdkGeoNamesErrorKind::Lookup,
- ..
- })
- ));
- assert!(matches!(
- sdk.farms()
- .upsert_private_location_with_geocoder(
- FarmPrivateLocationUpsertRequest::new(
- actor.clone(),
- "bad d tag",
- SdkExactLocation::new(12.26, -34.51),
- ),
- &geocoder,
- )
- .await,
- Err(RadrootsSdkError::InvalidRequest { .. })
- ));
- assert!(matches!(
- sdk.farms()
- .upsert_private_location_with_geocoder(
- FarmPrivateLocationUpsertRequest::new(
- actor.clone(),
- FARM_B_D_TAG,
- SdkExactLocation::new(12.26, -34.51),
- ),
- &blank_geocoder,
- )
- .await,
- Err(RadrootsSdkError::GeoNames {
- kind: crate::RadrootsSdkGeoNamesErrorKind::Lookup,
- ..
- })
- ));
- let clock_error_sdk = crate::RadrootsClient::builder()
- .clock(crate::RadrootsSdkClock::BeforeUnixEpoch)
- .build()
- .await
- .expect("clock error sdk");
- assert!(matches!(
- clock_error_sdk
- .farms()
- .upsert_private_location_with_geocoder(
- FarmPrivateLocationUpsertRequest::new(
- actor.clone(),
- FARM_B_D_TAG,
- SdkExactLocation::new(12.26, -34.51),
- ),
- &geocoder,
- )
- .await,
- Err(RadrootsSdkError::ClockBeforeUnixEpoch)
- ));
-
- sdk._private_store.pool().close().await;
- assert!(matches!(
- sdk.farms()
- .upsert_private_location_with_geocoder(
- FarmPrivateLocationUpsertRequest::new(
- actor.clone(),
- FARM_C_D_TAG,
- SdkExactLocation::new(12.26, -34.51),
- ),
- &geocoder,
- )
- .await,
- Err(RadrootsSdkError::PrivateStore { .. })
- ));
- assert!(matches!(
- sdk.farms().private_location(&farm_a_addr).await,
- Err(RadrootsSdkError::PrivateStore { .. })
- ));
- assert!(matches!(
- sdk.farms()
- .clear_private_location(FarmPrivateLocationClearRequest::new(actor, FARM_A_D_TAG))
- .await,
- Err(RadrootsSdkError::PrivateStore { .. })
- ));
-}