commit 62d17daf5d03f46fc6ff8f05d2923ed131cc8607
parent 9d1b24ff21ade68c4b98a1ad2d7ffaec94e6aea6
Author: triesap <tyson@radroots.org>
Date: Thu, 18 Jun 2026 21:04:36 -0700
cli: migrate farm publish to sdk
Route farm publish through the SDK farm runtime for prepare, enqueue, and outbox push. Keep profile publication explicit as non-submitted in this workflow, remove the migrated farm path from the direct-relay allowlist, and guard the SDK-backed path against legacy writer regressions.
Update CLI integration coverage for SDK push failure, profile non-submission, and no legacy signed-event local writes on farm publish.
Diffstat:
5 files changed, 493 insertions(+), 843 deletions(-)
diff --git a/src/ops/exec/farm.rs b/src/ops/exec/farm.rs
@@ -185,7 +185,7 @@ impl OperationService<FarmPublishRequest> for FarmOperationService<'_> {
}
let view = crate::runtime::farm::publish(self.config, &args).map_err(|error| {
- OperationAdapterError::runtime_failure(request.operation_id(), error)
+ OperationAdapterError::sdk_adapter_failure(request.operation_id(), error)
})?;
farm_publish_result(request.operation_id(), &view)
}
diff --git a/src/runtime/farm.rs b/src/runtime/farm.rs
@@ -1,18 +1,20 @@
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
+use radroots_authority::{RadrootsActorContext, RadrootsLocalEventSigner};
+use radroots_events::contract::RadrootsActorRole;
use radroots_events::farm::{RadrootsFarm, RadrootsFarmLocation};
use radroots_events::kinds::{KIND_FARM, KIND_PROFILE};
use radroots_events::listing::RadrootsListingLocation;
use radroots_events::profile::{RadrootsProfile, RadrootsProfileType};
use radroots_events_codec::d_tag::is_d_tag_base64url;
-use radroots_events_codec::farm::encode::to_wire_parts_with_kind;
use radroots_events_codec::profile::encode::to_wire_parts_with_profile_type;
-use radroots_events_codec::wire::WireEventParts;
-use radroots_nostr::prelude::radroots_event_from_nostr;
-use radroots_replica_db::migrations;
-use radroots_replica_sync::{RadrootsReplicaIngestOutcome, radroots_replica_ingest_event};
-use radroots_sql_core::SqliteExecutor;
+use radroots_nostr::prelude::RadrootsNostrKeys;
+use radroots_sdk::{
+ FarmEnqueuePublishRequest, FarmEnqueueReceipt, FarmPreparePublishRequest, FarmPublishPlan,
+ PushOutboxEventReceipt, PushOutboxEventState, PushOutboxReceipt, PushOutboxRelayOutcomeKind,
+ PushOutboxRequest, SdkMutationState, SdkRelayTargetPolicy,
+};
use serde_json::json;
use crate::cli::global::{
@@ -24,30 +26,28 @@ use crate::runtime::account::{self, AccountRecordView};
use crate::runtime::config::{
PublishMode, RADROOTSD_PUBLISH_DEFERRED_REASON, RuntimeConfig, SignerBackend,
};
-use crate::runtime::direct_relay::{
- DirectRelayFailure, DirectRelayPublishError, DirectRelayPublishReceipt,
- publish_signed_event_with_identity, sign_parts_with_identity,
-};
use crate::runtime::farm_config::{
self, FarmConfigDocument, FarmConfigScope, FarmConfigSelection, FarmListingDefaults,
FarmMissingField, FarmPublicationStatus, ResolvedFarmConfig, SUPPORTED_FARM_CONFIG_VERSION,
};
-use crate::runtime::local_events::{
- append_local_work, append_signed_event, mark_signed_event_acknowledged,
- mark_signed_event_failed_for_publish_error,
-};
+use crate::runtime::local_events::append_local_work;
+use crate::runtime::sdk::{CliSdkAdapterError, CliSdkSession, sdk_relay_url_policy};
use crate::runtime::signer::{ActorWriteBindingError, resolve_actor_write_authority};
use crate::view::runtime::{
FarmConfigDocumentView, FarmConfigSummaryView, FarmGetView, FarmListingDefaultsView,
- FarmPublicationView, FarmPublishComponentView, FarmPublishEventView,
- FarmPublishLocalReplicaView, FarmPublishView, FarmRebindView, FarmSelectionView, FarmSetView,
- FarmSetupView, FarmStatusView, RelayFailureView,
+ FarmPublicationView, FarmPublishComponentView, FarmPublishEventView, FarmPublishView,
+ FarmRebindView, FarmSelectionView, FarmSetView, FarmSetupView, FarmStatusView,
+ RelayFailureView,
};
const FARM_CONFIG_SOURCE: &str = "farm config · local first";
const FARM_SELLER_ACTOR_SOURCE: &str = "farm_config";
-const RELAY_FARM_WRITE_SOURCE: &str = "direct Nostr relay publish · local key";
+const SDK_FARM_WRITE_SOURCE: &str = "SDK farm publish · local key";
const RADROOTSD_FARM_WRITE_SOURCE: &str = "radrootsd publish transport · deferred";
+const SDK_PROFILE_NOT_SUBMITTED_METHOD: &str = "sdk.farm.profile.not_submitted";
+const SDK_FARM_PUBLISH_METHOD: &str = "sdk.farm.publish.v1";
+const SDK_PROFILE_NOT_SUBMITTED_REASON: &str =
+ "profile publish is not part of SDK farm.publish.v1; profile draft was not submitted";
const RADROOTSD_BRIDGE_PROFILE_PUBLISH_METHOD: &str = "bridge.profile.publish";
const RADROOTSD_BRIDGE_FARM_PUBLISH_METHOD: &str = "bridge.farm.publish";
@@ -574,7 +574,7 @@ fn radrootsd_farm_publish_readiness(_config: &RuntimeConfig) -> FarmPublishReadi
pub fn publish(
config: &RuntimeConfig,
args: &FarmPublishArgs,
-) -> Result<FarmPublishView, RuntimeError> {
+) -> Result<FarmPublishView, CliSdkAdapterError> {
let scope = scope_from_arg(args.scope);
let resolved_scope = farm_config::resolve_scope(&config.paths, scope)?;
let path = farm_config::config_path(&config.paths, resolved_scope)?;
@@ -640,19 +640,32 @@ pub fn publish(
let farm_idempotency_key = component_idempotency_key(args, "farm")?;
if config.output.dry_run {
- return dry_run_publish_view(
- config,
- args,
- &resolved,
- &account_pubkey,
- previews,
- profile_idempotency_key,
- farm_idempotency_key,
- );
+ return match config.publish.mode {
+ PublishMode::NostrRelay => publish_via_sdk(
+ config,
+ args,
+ resolved,
+ account_pubkey,
+ previews,
+ profile_idempotency_key,
+ farm_idempotency_key,
+ ),
+ PublishMode::Radrootsd => Ok(radrootsd_preflight_publish_view(
+ config,
+ args,
+ &resolved,
+ &account_pubkey,
+ previews,
+ profile_idempotency_key,
+ farm_idempotency_key,
+ "unavailable",
+ RADROOTSD_PUBLISH_DEFERRED_REASON,
+ )),
+ };
}
match config.publish.mode {
- PublishMode::NostrRelay => publish_via_direct_relay(
+ PublishMode::NostrRelay => publish_via_sdk(
config,
args,
resolved,
@@ -675,245 +688,111 @@ pub fn publish(
}
}
-fn dry_run_publish_view(
+fn publish_via_sdk(
config: &RuntimeConfig,
args: &FarmPublishArgs,
- resolved: &ResolvedFarmConfig,
- account_pubkey: &str,
+ mut resolved: ResolvedFarmConfig,
+ account_pubkey: String,
previews: FarmPublishPreviews,
profile_idempotency_key: Option<String>,
farm_idempotency_key: Option<String>,
-) -> Result<FarmPublishView, RuntimeError> {
- match config.publish.mode {
- PublishMode::NostrRelay => {
- if let Err(error) = resolve_farm_signing_identity(
- config,
- resolved.document.selection.account.as_str(),
- account_pubkey,
- ) {
- return match error {
- ActorWriteBindingError::Account(failure) => Err(failure.into()),
- error => Ok(binding_error_publish_view(
- config,
- args,
- resolved,
- account_pubkey,
- previews,
- profile_idempotency_key,
- farm_idempotency_key,
- error,
- )),
- };
- }
-
- Ok(base_publish_view(
- "dry_run",
- config,
- args,
- resolved,
- account_pubkey,
- preview_component(
- "relay.profile.publish",
- KIND_PROFILE,
- profile_idempotency_key,
+) -> Result<FarmPublishView, CliSdkAdapterError> {
+ let input = sdk_farm_publish_input(&resolved, account_pubkey.as_str())?;
+ if config.output.dry_run {
+ if let Err(error) = resolve_farm_signing_identity(
+ config,
+ resolved.document.selection.account.as_str(),
+ account_pubkey.as_str(),
+ ) {
+ return match error {
+ ActorWriteBindingError::Account(failure) => Err(RuntimeError::from(failure).into()),
+ error => Ok(binding_error_publish_view(
+ config,
args,
- Some(previews.profile.event),
- ),
- preview_component(
- "relay.farm.publish",
- KIND_FARM,
+ &resolved,
+ &account_pubkey,
+ previews,
+ profile_idempotency_key,
farm_idempotency_key,
- args,
- Some(previews.farm.event),
- ),
- Some("dry run requested; relay publish skipped".to_owned()),
- vec!["radroots farm publish".to_owned()],
- ))
+ error,
+ )),
+ };
}
- PublishMode::Radrootsd => Ok(radrootsd_preflight_publish_view(
+
+ let session = CliSdkSession::connect_memory(config)?;
+ let plan = session
+ .sdk()
+ .farms()
+ .prepare_publish(FarmPreparePublishRequest::new(input.actor, input.farm))?;
+ return Ok(sdk_prepared_publish_view(
config,
args,
- resolved,
- account_pubkey,
+ &resolved,
+ account_pubkey.as_str(),
previews,
profile_idempotency_key,
farm_idempotency_key,
- "unavailable",
- RADROOTSD_PUBLISH_DEFERRED_REASON,
- )),
+ plan,
+ ));
}
-}
-fn publish_via_direct_relay(
- config: &RuntimeConfig,
- args: &FarmPublishArgs,
- mut resolved: ResolvedFarmConfig,
- account_pubkey: String,
- mut previews: FarmPublishPreviews,
- profile_idempotency_key: Option<String>,
- farm_idempotency_key: Option<String>,
-) -> Result<FarmPublishView, RuntimeError> {
- let signing = match resolve_farm_signing_identity(
+ let session = CliSdkSession::connect(config)?;
+ let signer = sdk_farm_signer(
config,
resolved.document.selection.account.as_str(),
account_pubkey.as_str(),
- ) {
- Ok(signing) => signing,
- Err(ActorWriteBindingError::Account(failure)) => return Err(failure.into()),
- Err(error) => {
- return Ok(binding_error_publish_view(
- config,
- args,
- &resolved,
- &account_pubkey,
- previews,
- profile_idempotency_key,
- farm_idempotency_key,
- error,
- ));
- }
- };
-
- if config.relay.urls.is_empty() {
- return Err(RuntimeError::Network(
- DirectRelayPublishError::MissingRelays.to_string(),
- ));
- }
-
- let profile_event = sign_parts_with_identity(&signing.identity, previews.profile.parts.clone())
- .map_err(|error| RuntimeError::Network(error.to_string()))?;
- previews.profile.event.event_id = Some(profile_event.id.to_hex());
- let profile_record = append_signed_event(
- config,
- format!("farm_profile:{}", resolved.document.selection.farm_d_tag).as_str(),
- Some(resolved.document.selection.account.clone()),
- Some(account_pubkey.clone()),
- Some(resolved.document.selection.farm_d_tag.clone()),
- None,
- &profile_event,
)?;
- let profile_receipt = match publish_signed_event_with_identity(
- &signing.identity,
- &config.relay.urls,
- profile_event,
- ) {
- Ok(receipt) => {
- mark_signed_event_acknowledged(
- config,
- profile_record.record_id.as_str(),
- receipt.target_relays.clone(),
- receipt.connected_relays.clone(),
- receipt.acknowledged_relays.clone(),
- receipt.failed_relays.clone(),
- )?;
- receipt
- }
- Err(error) => {
- mark_signed_event_failed_for_publish_error(
- config,
- profile_record.record_id.as_str(),
- &error,
- )?;
- return Err(RuntimeError::Network(error.to_string()));
- }
- };
- let profile_local_replica =
- farm_local_replica_ingest_view(config, "profile", &profile_receipt, None);
- persist_profile_publication(config, &mut resolved, profile_receipt.event_id.clone())?;
-
- let farm_event = sign_parts_with_identity(&signing.identity, previews.farm.parts.clone())
- .map_err(|error| RuntimeError::Network(error.to_string()))?;
- previews.farm.event.event_id = Some(farm_event.id.to_hex());
- let farm_record = append_signed_event(
- config,
- format!("farm:{}", resolved.document.selection.farm_d_tag).as_str(),
- Some(resolved.document.selection.account.clone()),
- Some(account_pubkey.clone()),
- Some(resolved.document.selection.farm_d_tag.clone()),
- None,
- &farm_event,
- )?;
- let farm_receipt =
- match publish_signed_event_with_identity(&signing.identity, &config.relay.urls, farm_event)
- {
- Ok(receipt) => {
- mark_signed_event_acknowledged(
- config,
- farm_record.record_id.as_str(),
- receipt.target_relays.clone(),
- receipt.connected_relays.clone(),
- receipt.acknowledged_relays.clone(),
- receipt.failed_relays.clone(),
- )?;
- receipt
- }
- Err(error) => {
- mark_signed_event_failed_for_publish_error(
- config,
- farm_record.record_id.as_str(),
- &error,
- )?;
- return Ok(partial_publish_view(
- config,
- args,
- &resolved,
- &account_pubkey,
- previews,
- profile_idempotency_key,
- farm_idempotency_key,
- profile_receipt,
- profile_local_replica,
- error,
- ));
- }
- };
- let farm_local_replica = farm_local_replica_ingest_view(
- config,
- "farm",
- &farm_receipt,
- previews.farm.event.event_addr.clone(),
+ let mut request = FarmEnqueuePublishRequest::new(
+ input.actor,
+ input.farm,
+ SdkRelayTargetPolicy::UseConfiguredRelays,
);
- persist_farm_publication(config, &mut resolved, farm_receipt.event_id.clone())?;
-
- let mut view = base_publish_view(
- "published",
+ if let Some(idempotency_key) = farm_idempotency_key.as_deref() {
+ request = request.try_with_idempotency_key(idempotency_key)?;
+ }
+ let enqueue = session.block_on(session.sdk().farms().enqueue_publish(request, &signer))?;
+ let push = session.block_on(
+ session.sdk().sync().push_outbox(
+ PushOutboxRequest::new()
+ .with_limit(1)
+ .with_relay_url_policy(sdk_relay_url_policy(config)),
+ ),
+ )?;
+ let view = sdk_enqueued_publish_view(
config,
args,
&resolved,
- &account_pubkey,
- published_component(
- "relay.profile.publish",
- KIND_PROFILE,
- profile_idempotency_key,
- args,
- previews.profile.event,
- profile_receipt,
- ),
- published_component(
- "relay.farm.publish",
- KIND_FARM,
- farm_idempotency_key,
- args,
- previews.farm.event,
- farm_receipt,
- ),
- None,
- Vec::new(),
+ account_pubkey.as_str(),
+ previews,
+ profile_idempotency_key,
+ farm_idempotency_key,
+ &enqueue,
+ &push,
);
- view.local_replica = vec![profile_local_replica, farm_local_replica];
+ if view.farm.state == "published" {
+ persist_farm_publication(
+ config,
+ &mut resolved,
+ enqueue.signed_event_id.as_str().to_owned(),
+ )?;
+ }
Ok(view)
}
#[derive(Debug, Clone)]
+struct SdkFarmPublishInput {
+ actor: RadrootsActorContext,
+ farm: RadrootsFarm,
+}
+
+#[derive(Debug, Clone)]
struct FarmPublishPreviews {
profile: FarmPublishEventDraft,
- farm: FarmPublishEventDraft,
}
#[derive(Debug, Clone)]
struct FarmPublishEventDraft {
event: FarmPublishEventView,
- parts: WireEventParts,
}
impl FarmPublishView {
@@ -1036,12 +915,6 @@ fn build_publish_previews(
let profile_parts =
to_wire_parts_with_profile_type(&document.profile, Some(RadrootsProfileType::Farm))
.map_err(|error| RuntimeError::Config(format!("invalid farm profile: {error}")))?;
- let farm_parts = to_wire_parts_with_kind(&document.farm, KIND_FARM)
- .map_err(|error| RuntimeError::Config(format!("invalid farm contract: {error}")))?;
- let farm_addr = format!(
- "{}:{}:{}",
- farm_parts.kind, account_pubkey, document.farm.d_tag
- );
Ok(FarmPublishPreviews {
profile: FarmPublishEventDraft {
@@ -1053,18 +926,6 @@ fn build_publish_previews(
event_id: None,
event_addr: None,
},
- parts: profile_parts,
- },
- farm: FarmPublishEventDraft {
- event: FarmPublishEventView {
- kind: farm_parts.kind,
- author: account_pubkey.to_owned(),
- content: farm_parts.content.clone(),
- tags: farm_parts.tags.clone(),
- event_id: None,
- event_addr: Some(farm_addr),
- },
- parts: farm_parts,
},
})
}
@@ -1146,9 +1007,7 @@ fn binding_error_publish_view(
FarmPublishComponentView {
state: state.clone(),
reason: Some(reason.clone()),
- ..preview_component(
- "relay.profile.publish",
- KIND_PROFILE,
+ ..profile_not_submitted_component(
profile_idempotency_key,
args,
Some(previews.profile.event),
@@ -1158,11 +1017,11 @@ fn binding_error_publish_view(
state: state.clone(),
reason: Some(reason.clone()),
..preview_component(
- "relay.farm.publish",
+ farm_publish_rpc_method(config),
KIND_FARM,
farm_idempotency_key,
args,
- Some(previews.farm.event),
+ None,
)
},
Some(reason),
@@ -1170,7 +1029,37 @@ fn binding_error_publish_view(
)
}
-fn partial_publish_view(
+fn sdk_farm_publish_input(
+ resolved: &ResolvedFarmConfig,
+ account_pubkey: &str,
+) -> Result<SdkFarmPublishInput, RuntimeError> {
+ let actor = RadrootsActorContext::local_account(
+ account_pubkey,
+ resolved.document.selection.account.clone(),
+ [RadrootsActorRole::Farmer],
+ )
+ .map_err(|error| RuntimeError::Config(format!("invalid farm SDK actor: {error}")))?;
+ Ok(SdkFarmPublishInput {
+ actor,
+ farm: resolved.document.farm.clone(),
+ })
+}
+
+fn sdk_farm_signer(
+ config: &RuntimeConfig,
+ account_id: &str,
+ account_pubkey: &str,
+) -> Result<RadrootsLocalEventSigner, RuntimeError> {
+ let signing = match resolve_farm_signing_identity(config, account_id, account_pubkey) {
+ Ok(signing) => signing,
+ Err(ActorWriteBindingError::Account(failure)) => return Err(failure.into()),
+ Err(error) => return Err(RuntimeError::Config(error.reason())),
+ };
+ let keys: RadrootsNostrKeys = signing.identity.into_keys();
+ RadrootsLocalEventSigner::new(keys).map_err(|error| RuntimeError::Config(error.to_string()))
+}
+
+fn sdk_prepared_publish_view(
config: &RuntimeConfig,
args: &FarmPublishArgs,
resolved: &ResolvedFarmConfig,
@@ -1178,191 +1067,250 @@ fn partial_publish_view(
previews: FarmPublishPreviews,
profile_idempotency_key: Option<String>,
farm_idempotency_key: Option<String>,
- profile_receipt: DirectRelayPublishReceipt,
- profile_local_replica: FarmPublishLocalReplicaView,
- farm_error: DirectRelayPublishError,
+ plan: FarmPublishPlan,
) -> FarmPublishView {
- let reason = format!("farm publish failed after profile publish: {farm_error}");
- let mut view = base_publish_view(
- "partial",
+ base_publish_view(
+ "dry_run",
config,
args,
resolved,
account_pubkey,
- published_component(
- "relay.profile.publish",
- KIND_PROFILE,
+ profile_not_submitted_component(
profile_idempotency_key,
args,
- previews.profile.event,
- profile_receipt,
+ Some(previews.profile.event),
),
- failed_component(
- "relay.farm.publish",
- KIND_FARM,
- farm_idempotency_key,
- args,
- previews.farm.event,
- &config.relay.urls,
- farm_error,
- ),
- Some(reason),
+ FarmPublishComponentView {
+ state: "not_submitted".to_owned(),
+ reason: Some("dry run requested; SDK enqueue and relay push skipped".to_owned()),
+ signer_mode: Some(config.signer.backend.as_str().to_owned()),
+ event_id: Some(plan.expected_event_id.as_str().to_owned()),
+ event_addr: Some(plan.farm_addr.as_str().to_owned()),
+ event: args.print_event.then_some(sdk_plan_event_view(&plan)),
+ ..preview_component(
+ farm_publish_rpc_method(config),
+ KIND_FARM,
+ farm_idempotency_key,
+ args,
+ None,
+ )
+ },
+ Some("dry run requested; SDK enqueue and relay push skipped".to_owned()),
vec!["radroots farm publish".to_owned()],
- );
- view.local_replica = vec![profile_local_replica];
- view
+ )
}
-fn published_component(
- rpc_method: &str,
- event_kind: u32,
- idempotency_key: Option<String>,
+fn sdk_enqueued_publish_view(
+ config: &RuntimeConfig,
args: &FarmPublishArgs,
- mut event: FarmPublishEventView,
- receipt: DirectRelayPublishReceipt,
-) -> FarmPublishComponentView {
- event.event_id = Some(receipt.event_id.clone());
- FarmPublishComponentView {
- state: "published".to_owned(),
- rpc_method: rpc_method.to_owned(),
- event_kind,
- deduplicated: false,
- target_relays: receipt.target_relays,
- connected_relays: receipt.connected_relays,
- acknowledged_relays: receipt.acknowledged_relays,
- failed_relays: relay_failures(receipt.failed_relays),
- job_id: None,
- job_status: None,
- signer_mode: Some("local".to_owned()),
- signer_session_id: None,
- event_id: Some(receipt.event_id),
- event_addr: event.event_addr.clone(),
- idempotency_key,
- reason: None,
- job: None,
- event: args.print_event.then_some(event),
- }
+ resolved: &ResolvedFarmConfig,
+ account_pubkey: &str,
+ previews: FarmPublishPreviews,
+ profile_idempotency_key: Option<String>,
+ farm_idempotency_key: Option<String>,
+ enqueue: &FarmEnqueueReceipt,
+ push: &PushOutboxReceipt,
+) -> FarmPublishView {
+ let push_event = sdk_push_event_for_farm(enqueue, push);
+ let state = sdk_publish_state(push_event);
+ let view_state = state.clone();
+ let reason = sdk_publish_reason(push_event);
+ base_publish_view(
+ view_state.as_str(),
+ config,
+ args,
+ resolved,
+ account_pubkey,
+ profile_not_submitted_component(
+ profile_idempotency_key,
+ args,
+ Some(previews.profile.event),
+ ),
+ FarmPublishComponentView {
+ state,
+ deduplicated: matches!(enqueue.state, SdkMutationState::AlreadyQueued),
+ target_relays: push_event
+ .map(sdk_push_target_relays)
+ .unwrap_or_else(|| config.relay.urls.clone()),
+ connected_relays: push_event
+ .map(sdk_push_connected_relays)
+ .unwrap_or_default(),
+ acknowledged_relays: push_event
+ .map(sdk_push_acknowledged_relays)
+ .unwrap_or_default(),
+ failed_relays: push_event.map(sdk_push_failed_relays).unwrap_or_default(),
+ signer_mode: Some(config.signer.backend.as_str().to_owned()),
+ event_id: Some(enqueue.signed_event_id.as_str().to_owned()),
+ event_addr: Some(enqueue.farm_addr.as_str().to_owned()),
+ idempotency_key: farm_idempotency_key,
+ reason: sdk_publish_reason(push_event),
+ ..preview_component(farm_publish_rpc_method(config), KIND_FARM, None, args, None)
+ },
+ reason,
+ sdk_publish_actions(push_event),
+ )
}
-fn farm_local_replica_ingest_view(
- config: &RuntimeConfig,
- component: &str,
- receipt: &DirectRelayPublishReceipt,
- event_addr: Option<String>,
-) -> FarmPublishLocalReplicaView {
- if !config.local.replica_db_path.exists() {
- return FarmPublishLocalReplicaView {
- component: component.to_owned(),
- state: "unconfigured".to_owned(),
- store_state: "missing".to_owned(),
- ingest_outcome: None,
- event_id: Some(receipt.event_id.clone()),
- event_addr,
- reason: Some("local replica database is not initialized".to_owned()),
- actions: vec!["radroots store init".to_owned()],
- };
+fn sdk_plan_event_view(plan: &FarmPublishPlan) -> FarmPublishEventView {
+ FarmPublishEventView {
+ kind: plan.frozen_draft.kind,
+ author: plan.frozen_draft.expected_pubkey.clone(),
+ content: plan.frozen_draft.content.clone(),
+ tags: plan.frozen_draft.tags.clone(),
+ event_id: Some(plan.expected_event_id.as_str().to_owned()),
+ event_addr: Some(plan.farm_addr.as_str().to_owned()),
}
+}
- let executor = match SqliteExecutor::open(&config.local.replica_db_path) {
- Ok(executor) => executor,
- Err(error) => {
- return farm_local_replica_failed_view(
- component,
- receipt.event_id.clone(),
- event_addr,
- format!("failed to open local replica database: {error}"),
- );
+fn sdk_push_event_for_farm<'a>(
+ enqueue: &FarmEnqueueReceipt,
+ push: &'a PushOutboxReceipt,
+) -> Option<&'a PushOutboxEventReceipt> {
+ push.events
+ .iter()
+ .find(|event| event.event_id == enqueue.signed_event_id)
+}
+
+fn sdk_publish_state(push_event: Option<&PushOutboxEventReceipt>) -> String {
+ match push_event.map(|event| event.final_state) {
+ Some(PushOutboxEventState::Published) => "published",
+ Some(PushOutboxEventState::PublishRetryable | PushOutboxEventState::FailedTerminal) => {
+ "unavailable"
}
- };
- if let Err(error) = migrations::run_all_up(&executor) {
- return farm_local_replica_failed_view(
- component,
- receipt.event_id.clone(),
- event_addr,
- format!("failed to migrate local replica database: {error}"),
- );
+ Some(_) | None => "queued",
}
+ .to_owned()
+}
- let event = radroots_event_from_nostr(&receipt.event);
- match radroots_replica_ingest_event(&executor, &event) {
- Ok(RadrootsReplicaIngestOutcome::Applied) => FarmPublishLocalReplicaView {
- component: component.to_owned(),
- state: "applied".to_owned(),
- store_state: "ready".to_owned(),
- ingest_outcome: Some("applied".to_owned()),
- event_id: Some(receipt.event_id.clone()),
- event_addr,
- reason: None,
- actions: Vec::new(),
- },
- Ok(RadrootsReplicaIngestOutcome::Skipped) => FarmPublishLocalReplicaView {
- component: component.to_owned(),
- state: "skipped".to_owned(),
- store_state: "ready".to_owned(),
- ingest_outcome: Some("skipped".to_owned()),
- event_id: Some(receipt.event_id.clone()),
- event_addr,
- reason: Some("shared replica ingest skipped the event".to_owned()),
- actions: Vec::new(),
- },
- Err(error) => farm_local_replica_failed_view(
- component,
- receipt.event_id.clone(),
- event_addr,
- format!("failed to ingest farm publish event into local replica: {error}"),
+fn sdk_publish_reason(push_event: Option<&PushOutboxEventReceipt>) -> Option<String> {
+ match push_event.map(|event| event.final_state) {
+ Some(PushOutboxEventState::Published) => None,
+ Some(PushOutboxEventState::PublishRetryable) => Some(
+ "SDK relay publish did not reach accepted quorum; outbox event remains retryable"
+ .to_owned(),
+ ),
+ Some(PushOutboxEventState::FailedTerminal) => {
+ Some("SDK relay publish failed terminally".to_owned())
+ }
+ Some(state) => Some(format!("SDK relay push left event in state `{state:?}`")),
+ None => Some(
+ "farm publish queued in SDK outbox; no ready SDK outbox event was pushed".to_owned(),
),
}
}
-fn farm_local_replica_failed_view(
- component: &str,
- event_id: String,
- event_addr: Option<String>,
- reason: String,
-) -> FarmPublishLocalReplicaView {
- FarmPublishLocalReplicaView {
- component: component.to_owned(),
- state: "failed".to_owned(),
- store_state: "unavailable".to_owned(),
- ingest_outcome: None,
- event_id: Some(event_id),
- event_addr,
- reason: Some(reason),
- actions: vec!["radroots store status get".to_owned()],
+fn sdk_publish_actions(push_event: Option<&PushOutboxEventReceipt>) -> Vec<String> {
+ if !matches!(
+ push_event.map(|event| event.final_state),
+ Some(PushOutboxEventState::Published)
+ ) {
+ return vec!["radroots sync push".to_owned()];
}
+ Vec::new()
+}
+
+fn sdk_push_target_relays(event: &PushOutboxEventReceipt) -> Vec<String> {
+ event
+ .relays
+ .iter()
+ .map(|relay| relay.relay_url.clone())
+ .collect()
+}
+
+fn sdk_push_connected_relays(event: &PushOutboxEventReceipt) -> Vec<String> {
+ event
+ .relays
+ .iter()
+ .filter(|relay| relay.attempted)
+ .map(|relay| relay.relay_url.clone())
+ .collect()
+}
+
+fn sdk_push_acknowledged_relays(event: &PushOutboxEventReceipt) -> Vec<String> {
+ event
+ .relays
+ .iter()
+ .filter(|relay| {
+ matches!(
+ relay.outcome_kind,
+ PushOutboxRelayOutcomeKind::Accepted
+ | PushOutboxRelayOutcomeKind::DuplicateAccepted
+ )
+ })
+ .map(|relay| relay.relay_url.clone())
+ .collect()
+}
+
+fn sdk_push_failed_relays(event: &PushOutboxEventReceipt) -> Vec<RelayFailureView> {
+ event
+ .relays
+ .iter()
+ .filter(|relay| {
+ !matches!(
+ relay.outcome_kind,
+ PushOutboxRelayOutcomeKind::Accepted
+ | PushOutboxRelayOutcomeKind::DuplicateAccepted
+ )
+ })
+ .map(|relay| RelayFailureView {
+ relay: relay.relay_url.clone(),
+ reason: relay
+ .message
+ .clone()
+ .unwrap_or_else(|| sdk_relay_outcome_kind(relay.outcome_kind).to_owned()),
+ })
+ .collect()
}
-fn failed_component(
+fn sdk_relay_outcome_kind(kind: PushOutboxRelayOutcomeKind) -> &'static str {
+ match kind {
+ PushOutboxRelayOutcomeKind::Accepted => "accepted",
+ PushOutboxRelayOutcomeKind::DuplicateAccepted => "duplicate_accepted",
+ PushOutboxRelayOutcomeKind::Blocked => "blocked",
+ PushOutboxRelayOutcomeKind::RateLimited => "rate_limited",
+ PushOutboxRelayOutcomeKind::Invalid => "invalid",
+ PushOutboxRelayOutcomeKind::PowRequired => "pow_required",
+ PushOutboxRelayOutcomeKind::Restricted => "restricted",
+ PushOutboxRelayOutcomeKind::AuthRequired => "auth_required",
+ PushOutboxRelayOutcomeKind::Error => "error",
+ PushOutboxRelayOutcomeKind::Timeout => "timeout",
+ PushOutboxRelayOutcomeKind::ConnectionFailed => "connection_failed",
+ PushOutboxRelayOutcomeKind::Unknown => "unknown",
+ _ => "unknown",
+ }
+}
+
+fn profile_not_submitted_component(
+ idempotency_key: Option<String>,
+ args: &FarmPublishArgs,
+ event: Option<FarmPublishEventView>,
+) -> FarmPublishComponentView {
+ FarmPublishComponentView {
+ reason: Some(SDK_PROFILE_NOT_SUBMITTED_REASON.to_owned()),
+ ..preview_component(
+ SDK_PROFILE_NOT_SUBMITTED_METHOD,
+ KIND_PROFILE,
+ idempotency_key,
+ args,
+ event,
+ )
+ }
+}
+
+fn deferred_component(
rpc_method: &str,
event_kind: u32,
idempotency_key: Option<String>,
args: &FarmPublishArgs,
- event: FarmPublishEventView,
- relay_urls: &[String],
- error: DirectRelayPublishError,
+ reason: &str,
+ event: Option<FarmPublishEventView>,
) -> FarmPublishComponentView {
- let reason = error.to_string();
- let failure = publish_failure_details(&error, relay_urls);
- let event_id = failure.event_id.or_else(|| event.event_id.clone());
FarmPublishComponentView {
- state: "failed".to_owned(),
- rpc_method: rpc_method.to_owned(),
- event_kind,
- deduplicated: false,
- target_relays: failure.target_relays,
- connected_relays: failure.connected_relays,
- acknowledged_relays: Vec::new(),
- failed_relays: failure.failed_relays,
- job_id: None,
- job_status: None,
- signer_mode: Some("local".to_owned()),
+ state: "unavailable".to_owned(),
+ signer_mode: Some("deferred".to_owned()),
signer_session_id: None,
- event_id,
- event_addr: event.event_addr.clone(),
- idempotency_key,
- reason: Some(reason),
- job: None,
- event: args.print_event.then_some(event),
+ reason: Some(reason.to_owned()),
+ ..preview_component(rpc_method, event_kind, idempotency_key, args, event)
}
}
@@ -1384,32 +1332,22 @@ fn radrootsd_preflight_publish_view(
args,
resolved,
account_pubkey,
- FarmPublishComponentView {
- state: state.to_owned(),
- signer_mode: Some("deferred".to_owned()),
- signer_session_id: None,
- reason: Some(reason.to_owned()),
- ..radrootsd_preview_component(
- RADROOTSD_BRIDGE_PROFILE_PUBLISH_METHOD,
- KIND_PROFILE,
- profile_idempotency_key,
- args,
- Some(previews.profile.event),
- )
- },
- FarmPublishComponentView {
- state: state.to_owned(),
- signer_mode: Some("deferred".to_owned()),
- signer_session_id: None,
- reason: Some(reason.to_owned()),
- ..radrootsd_preview_component(
- RADROOTSD_BRIDGE_FARM_PUBLISH_METHOD,
- KIND_FARM,
- farm_idempotency_key,
- args,
- Some(previews.farm.event),
- )
- },
+ deferred_component(
+ RADROOTSD_BRIDGE_PROFILE_PUBLISH_METHOD,
+ KIND_PROFILE,
+ profile_idempotency_key,
+ args,
+ reason,
+ Some(previews.profile.event),
+ ),
+ deferred_component(
+ RADROOTSD_BRIDGE_FARM_PUBLISH_METHOD,
+ KIND_FARM,
+ farm_idempotency_key,
+ args,
+ reason,
+ None,
+ ),
Some(reason.to_owned()),
vec![
"radroots --publish-mode nostr_relay --relay wss://relay.example.com farm publish"
@@ -1419,27 +1357,6 @@ fn radrootsd_preflight_publish_view(
.with_requested_signer_session_id(requested_signer_session_id)
}
-fn radrootsd_preview_component(
- rpc_method: &str,
- event_kind: u32,
- idempotency_key: Option<String>,
- args: &FarmPublishArgs,
- event: Option<FarmPublishEventView>,
-) -> FarmPublishComponentView {
- FarmPublishComponentView {
- signer_mode: Some("deferred".to_owned()),
- ..preview_component(rpc_method, event_kind, idempotency_key, args, event)
- }
-}
-
-fn persist_profile_publication(
- config: &RuntimeConfig,
- resolved: &mut ResolvedFarmConfig,
- event_id: String,
-) -> Result<(), RuntimeError> {
- persist_publication(config, resolved, Some(event_id), None)
-}
-
fn persist_farm_publication(
config: &RuntimeConfig,
resolved: &mut ResolvedFarmConfig,
@@ -1469,92 +1386,25 @@ fn persist_publication(
fn farm_write_source(config: &RuntimeConfig) -> &'static str {
match config.publish.mode {
- PublishMode::NostrRelay => RELAY_FARM_WRITE_SOURCE,
+ PublishMode::NostrRelay => SDK_FARM_WRITE_SOURCE,
PublishMode::Radrootsd => RADROOTSD_FARM_WRITE_SOURCE,
}
}
fn profile_publish_rpc_method(config: &RuntimeConfig) -> &'static str {
match config.publish.mode {
- PublishMode::NostrRelay => "relay.profile.publish",
+ PublishMode::NostrRelay => SDK_PROFILE_NOT_SUBMITTED_METHOD,
PublishMode::Radrootsd => RADROOTSD_BRIDGE_PROFILE_PUBLISH_METHOD,
}
}
fn farm_publish_rpc_method(config: &RuntimeConfig) -> &'static str {
match config.publish.mode {
- PublishMode::NostrRelay => "relay.farm.publish",
+ PublishMode::NostrRelay => SDK_FARM_PUBLISH_METHOD,
PublishMode::Radrootsd => RADROOTSD_BRIDGE_FARM_PUBLISH_METHOD,
}
}
-#[derive(Debug, Clone)]
-struct FarmPublishFailureDetails {
- event_id: Option<String>,
- target_relays: Vec<String>,
- connected_relays: Vec<String>,
- failed_relays: Vec<RelayFailureView>,
-}
-
-fn publish_failure_details(
- error: &DirectRelayPublishError,
- relay_urls: &[String],
-) -> FarmPublishFailureDetails {
- match error {
- DirectRelayPublishError::MissingRelays
- | DirectRelayPublishError::Runtime(_)
- | DirectRelayPublishError::Build(_)
- | DirectRelayPublishError::Sign(_) => FarmPublishFailureDetails {
- event_id: None,
- target_relays: relay_urls.to_vec(),
- connected_relays: Vec::new(),
- failed_relays: Vec::new(),
- },
- DirectRelayPublishError::RelayConfig { relay, source } => FarmPublishFailureDetails {
- event_id: None,
- target_relays: relay_urls.to_vec(),
- connected_relays: Vec::new(),
- failed_relays: vec![RelayFailureView {
- relay: relay.clone(),
- reason: source.to_string(),
- }],
- },
- DirectRelayPublishError::Connect {
- target_relays,
- connected_relays,
- failed_relays,
- ..
- } => FarmPublishFailureDetails {
- event_id: None,
- target_relays: target_relays.clone(),
- connected_relays: connected_relays.clone(),
- failed_relays: relay_failures(failed_relays.clone()),
- },
- DirectRelayPublishError::Publish {
- event_id,
- target_relays,
- connected_relays,
- failed_relays,
- ..
- } => FarmPublishFailureDetails {
- event_id: Some(event_id.clone()),
- target_relays: target_relays.clone(),
- connected_relays: connected_relays.clone(),
- failed_relays: relay_failures(failed_relays.clone()),
- },
- }
-}
-
-fn relay_failures(failures: Vec<DirectRelayFailure>) -> Vec<RelayFailureView> {
- failures
- .into_iter()
- .map(|failure| RelayFailureView {
- relay: failure.relay,
- reason: failure.reason,
- })
- .collect()
-}
-
fn selected_account_for_draft(
config: &RuntimeConfig,
) -> Result<Option<AccountRecordView>, RuntimeError> {
diff --git a/src/runtime/sdk.rs b/src/runtime/sdk.rs
@@ -358,16 +358,6 @@ mod tests {
const LEGACY_DIRECT_RELAY_CONSUMERS: &[LegacyDirectRelayConsumer] = &[
LegacyDirectRelayConsumer {
- path: "src/runtime/farm.rs",
- required_tokens: &[
- "publish_via_direct_relay(",
- "publish_signed_event_with_identity",
- ],
- owner: "farm.publish",
- reason: "non-migrated farm publish direct relay write mode",
- lifecycle: "retain until farm publish migrates to SDK-backed write APIs",
- },
- LegacyDirectRelayConsumer {
path: "src/runtime/listing.rs",
required_tokens: &[
"mutate_via_direct_relay(",
@@ -424,6 +414,17 @@ mod tests {
],
},
MigratedCliPathGuard {
+ label: "farm publish",
+ path: "src/runtime/farm.rs",
+ start: "fn publish_via_sdk(",
+ end: "#[derive(Debug, Clone)]\nstruct SdkFarmPublishInput",
+ required_tokens: &[
+ "prepare_publish(FarmPreparePublishRequest::new",
+ "enqueue_publish(request, &signer)",
+ "session.sdk().sync().push_outbox",
+ ],
+ },
+ MigratedCliPathGuard {
label: "sync status",
path: "src/runtime/sync.rs",
start: "pub fn status(config: &RuntimeConfig) -> Result<SyncStatusView, CliSdkAdapterError>",
diff --git a/tests/signer_runtime_modes.rs b/tests/signer_runtime_modes.rs
@@ -1,14 +1,9 @@
mod support;
use std::fs;
-use std::net::{TcpListener, TcpStream};
use std::path::Path;
-use std::sync::mpsc::{self, Receiver};
-use std::thread::{self, JoinHandle};
-use std::time::Duration;
-use radroots_events::kinds::{KIND_FARM, KIND_PROFILE};
-use serde_json::{Value, json};
+use serde_json::Value;
use support::{
RadrootsCliSandbox, assert_contains, assert_no_daemon_runtime_reference,
assert_no_removed_command_reference, create_listing_draft, identity_public, identity_secret,
@@ -19,87 +14,6 @@ use support::{
const LISTING_ADDR: &str =
"30402:1111111111111111111111111111111111111111111111111111111111111111:AAAAAAAAAAAAAAAAAAAAAg";
-struct FarmPartialRelayServer {
- endpoint: String,
- requests: Receiver<Value>,
- handle: JoinHandle<()>,
-}
-
-impl FarmPartialRelayServer {
- fn profile_accept_farm_reject() -> Self {
- Self::with_publish_outcomes([(true, ""), (false, "farm rejected by test relay")])
- }
-
- fn profile_and_farm_accept() -> Self {
- Self::with_publish_outcomes([(true, ""), (true, "")])
- }
-
- fn with_publish_outcomes(outcomes: [(bool, &'static str); 2]) -> Self {
- let listener = TcpListener::bind("127.0.0.1:0").expect("bind relay");
- let endpoint = format!("ws://{}", listener.local_addr().expect("relay addr"));
- let (tx, requests) = mpsc::channel();
- let handle = thread::spawn(move || {
- for (accepted, reason) in outcomes {
- let (stream, _) = listener.accept().expect("accept relay connection");
- handle_publish_connection(stream, accepted, reason, &tx);
- }
- });
-
- Self {
- endpoint,
- requests,
- handle,
- }
- }
-
- fn endpoint(&self) -> &str {
- self.endpoint.as_str()
- }
-
- fn take_requests(self) -> Vec<Value> {
- let requests = (0..2)
- .map(|_| {
- self.requests
- .recv_timeout(Duration::from_secs(5))
- .expect("relay publish request")
- })
- .collect::<Vec<_>>();
- self.handle.join().expect("relay server join");
- requests
- }
-}
-
-fn handle_publish_connection(
- stream: TcpStream,
- accepted: bool,
- reason: &str,
- tx: &mpsc::Sender<Value>,
-) {
- let mut websocket = tungstenite::accept(stream).expect("accept websocket");
- let event = read_event_message(&mut websocket);
- let event_id = event["id"].as_str().expect("event id").to_owned();
- tx.send(event).expect("relay request send");
- websocket
- .send(tungstenite::Message::Text(
- json!(["OK", event_id, accepted, reason]).to_string().into(),
- ))
- .expect("relay ok send");
-}
-
-fn read_event_message(websocket: &mut tungstenite::WebSocket<TcpStream>) -> Value {
- loop {
- let message = websocket.read().expect("relay message");
- if !message.is_text() {
- continue;
- }
- let value: Value =
- serde_json::from_str(message.to_text().expect("relay text")).expect("relay json");
- if value.get(0).and_then(Value::as_str) == Some("EVENT") {
- return value.get(1).cloned().expect("relay event payload");
- }
- }
-}
-
#[test]
fn local_signer_status_reports_unconfigured_without_account() {
let sandbox = RadrootsCliSandbox::new();
@@ -1277,7 +1191,7 @@ fn farm_setup_actions_withhold_publish_for_watch_only_account() {
}
#[test]
-fn local_farm_publish_reports_partial_when_farm_event_fails_after_profile_publish() {
+fn local_farm_publish_reports_sdk_push_failure_without_profile_publish() {
let sandbox = RadrootsCliSandbox::new();
sandbox.json_success(&["--format", "json", "account", "create"]);
sandbox.json_success(&[
@@ -1294,14 +1208,13 @@ fn local_farm_publish_reports_partial_when_farm_event_fails_after_profile_publis
"--delivery-method",
"pickup",
]);
- let relay = FarmPartialRelayServer::profile_accept_farm_reject();
- let relay_url = relay.endpoint().to_owned();
+ let relay_url = "ws://127.0.0.1:9";
let (output, value) = sandbox.json_output(&[
"--format",
"json",
"--relay",
- relay_url.as_str(),
+ relay_url,
"--approval-token",
"approve",
"--idempotency-key",
@@ -1309,7 +1222,6 @@ fn local_farm_publish_reports_partial_when_farm_event_fails_after_profile_publis
"farm",
"publish",
]);
- let requests = relay.take_requests();
assert!(!output.status.success());
assert_eq!(value["operation_id"], "farm.publish");
@@ -1318,45 +1230,33 @@ fn local_farm_publish_reports_partial_when_farm_event_fails_after_profile_publis
assert_eq!(value["errors"][0]["detail"]["class"], "network");
assert_contains(
&value["errors"][0]["message"],
- "farm publish failed after profile publish",
- );
- assert_contains(
- &value["errors"][0]["message"],
- "farm rejected by test relay",
+ "SDK relay publish did not reach accepted quorum",
);
let detail = &value["errors"][0]["detail"];
- assert_eq!(detail["state"], "partial");
- assert_eq!(detail["profile"]["state"], "published");
- assert_eq!(detail["farm"]["state"], "failed");
- assert_eq!(detail["profile"]["event_id"], requests[0]["id"]);
- assert_eq!(detail["farm"]["event_id"], requests[1]["id"]);
+ assert_eq!(detail["source"], "SDK farm publish · local key");
+ assert_eq!(detail["state"], "unavailable");
+ assert_eq!(detail["profile"]["state"], "not_submitted");
+ assert_eq!(detail["farm"]["state"], "unavailable");
+ assert_eq!(detail["profile"]["event_id"], serde_json::Value::Null);
+ assert_eq!(
+ detail["farm"]["event_id"]
+ .as_str()
+ .expect("sdk farm event id")
+ .len(),
+ 64
+ );
assert_eq!(detail["profile"]["idempotency_key"], "farm_partial:profile");
assert_eq!(detail["farm"]["idempotency_key"], "farm_partial:farm");
- assert_eq!(detail["actions"][0], "radroots farm publish");
- assert_eq!(detail["profile"]["target_relays"][0], relay_url.as_str());
- assert_eq!(detail["farm"]["target_relays"][0], relay_url.as_str());
- assert_relay_url(
- &detail["profile"]["acknowledged_relays"][0],
- relay_url.as_str(),
- );
- assert_relay_url(&detail["farm"]["connected_relays"][0], relay_url.as_str());
- assert_relay_url(
- &detail["farm"]["failed_relays"][0]["relay"],
- relay_url.as_str(),
- );
- assert_contains(
- &detail["farm"]["failed_relays"][0]["reason"],
- "farm rejected by test relay",
- );
- assert_eq!(requests[0]["kind"], KIND_PROFILE);
- assert_eq!(requests[1]["kind"], KIND_FARM);
+ assert_eq!(detail["actions"][0], "radroots sync push");
+ assert_eq!(detail["farm"]["target_relays"][0], relay_url);
+ assert_relay_url(&detail["farm"]["failed_relays"][0]["relay"], relay_url);
assert_no_removed_command_reference(&value, &["farm", "publish"]);
assert_no_daemon_runtime_reference(&value, &["farm", "publish"]);
let persisted = sandbox.json_success(&["--format", "json", "farm", "get"]);
assert_eq!(
persisted["result"]["document"]["publication"]["profile_state"],
- "published"
+ "not_published"
);
assert_eq!(
persisted["result"]["document"]["publication"]["farm_state"],
@@ -1364,12 +1264,16 @@ fn local_farm_publish_reports_partial_when_farm_event_fails_after_profile_publis
);
assert_eq!(
persisted["result"]["document"]["publication"]["profile_event_id"],
- requests[0]["id"]
+ serde_json::Value::Null
+ );
+ assert_eq!(
+ persisted["result"]["document"]["publication"]["farm_event_id"],
+ serde_json::Value::Null
);
}
#[test]
-fn local_farm_publish_persists_publication_after_profile_and_farm_publish() {
+fn local_farm_publish_does_not_persist_publication_until_sdk_push_publishes() {
let sandbox = RadrootsCliSandbox::new();
sandbox.json_success(&["--format", "json", "account", "create"]);
sandbox.json_success(&[
@@ -1386,14 +1290,13 @@ fn local_farm_publish_persists_publication_after_profile_and_farm_publish() {
"--delivery-method",
"pickup",
]);
- let relay = FarmPartialRelayServer::profile_and_farm_accept();
- let relay_url = relay.endpoint().to_owned();
+ let relay_url = "ws://127.0.0.1:9";
- let value = sandbox.json_success(&[
+ let (output, value) = sandbox.json_output(&[
"--format",
"json",
"--relay",
- relay_url.as_str(),
+ relay_url,
"--approval-token",
"approve",
"--idempotency-key",
@@ -1401,43 +1304,43 @@ fn local_farm_publish_persists_publication_after_profile_and_farm_publish() {
"farm",
"publish",
]);
- let requests = relay.take_requests();
+ assert!(!output.status.success());
assert_eq!(value["operation_id"], "farm.publish");
- assert_eq!(value["result"]["state"], "published");
- assert_eq!(value["result"]["profile"]["state"], "published");
- assert_eq!(value["result"]["farm"]["state"], "published");
- assert_eq!(value["result"]["profile"]["event_id"], requests[0]["id"]);
- assert_eq!(value["result"]["farm"]["event_id"], requests[1]["id"]);
- assert_eq!(
- value["result"]["profile"]["idempotency_key"],
- "farm_success:profile"
- );
+ assert_eq!(value["result"], serde_json::Value::Null);
+ let detail = &value["errors"][0]["detail"];
+ assert_eq!(detail["source"], "SDK farm publish · local key");
+ assert_eq!(detail["profile"]["state"], "not_submitted");
+ assert_eq!(detail["farm"]["state"], "unavailable");
+ assert_eq!(detail["profile"]["event_id"], serde_json::Value::Null);
assert_eq!(
- value["result"]["farm"]["idempotency_key"],
- "farm_success:farm"
+ detail["farm"]["event_id"]
+ .as_str()
+ .expect("sdk farm event id")
+ .len(),
+ 64
);
- assert_eq!(requests[0]["kind"], KIND_PROFILE);
- assert_eq!(requests[1]["kind"], KIND_FARM);
+ assert_eq!(detail["profile"]["idempotency_key"], "farm_success:profile");
+ assert_eq!(detail["farm"]["idempotency_key"], "farm_success:farm");
assert_no_removed_command_reference(&value, &["farm", "publish"]);
assert_no_daemon_runtime_reference(&value, &["farm", "publish"]);
let persisted = sandbox.json_success(&["--format", "json", "farm", "get"]);
assert_eq!(
persisted["result"]["document"]["publication"]["profile_state"],
- "published"
+ "not_published"
);
assert_eq!(
persisted["result"]["document"]["publication"]["farm_state"],
- "published"
+ "not_published"
);
assert_eq!(
persisted["result"]["document"]["publication"]["profile_event_id"],
- requests[0]["id"]
+ serde_json::Value::Null
);
assert_eq!(
persisted["result"]["document"]["publication"]["farm_event_id"],
- requests[1]["id"]
+ serde_json::Value::Null
);
}
@@ -1486,27 +1389,14 @@ fn farm_rebind_is_explicit_and_publish_defaults_ignore_ambient_selection() {
.is_none()
);
- let relay = FarmPartialRelayServer::profile_and_farm_accept();
- let relay_url = relay.endpoint().to_owned();
- sandbox.json_success(&[
- "--format",
- "json",
- "--relay",
- relay_url.as_str(),
- "--approval-token",
- "approve",
- "farm",
- "publish",
- ]);
- let _requests = relay.take_requests();
let published = sandbox.json_success(&["--format", "json", "farm", "get"]);
assert_eq!(
published["result"]["document"]["publication"]["profile_state"],
- "published"
+ "not_published"
);
assert_eq!(
published["result"]["document"]["publication"]["farm_state"],
- "published"
+ "not_published"
);
let same_seller_dry_run = sandbox.json_success(&[
@@ -1540,11 +1430,11 @@ fn farm_rebind_is_explicit_and_publish_defaults_ignore_ambient_selection() {
let same_seller_get = sandbox.json_success(&["--format", "json", "farm", "get"]);
assert_eq!(
same_seller_get["result"]["document"]["publication"]["profile_state"],
- "published"
+ "not_published"
);
assert_eq!(
same_seller_get["result"]["document"]["publication"]["farm_state"],
- "published"
+ "not_published"
);
let second = sandbox.json_success(&["--format", "json", "account", "create"]);
@@ -1978,7 +1868,7 @@ fn farm_rebind_allows_watch_only_target_and_attach_secret_recovers_publish() {
}
#[test]
-fn local_seller_publish_commands_attempt_configured_direct_relay() {
+fn local_seller_publish_commands_attempt_configured_relay() {
let sandbox = RadrootsCliSandbox::new();
sandbox.json_success(&["--format", "json", "account", "create"]);
let farm = sandbox.json_success(&[
@@ -2011,7 +1901,28 @@ fn local_seller_publish_commands_attempt_configured_direct_relay() {
"publish",
]);
assert!(!farm_output.status.success());
- assert_direct_relay_connection_failure(&farm_value, "farm.publish", &["farm", "publish"]);
+ assert_eq!(farm_value["operation_id"], "farm.publish");
+ assert_eq!(farm_value["result"], serde_json::Value::Null);
+ assert_eq!(farm_value["errors"][0]["code"], "network_unavailable");
+ assert_eq!(farm_value["errors"][0]["detail"]["class"], "network");
+ assert_contains(
+ &farm_value["errors"][0]["message"],
+ "SDK relay publish did not reach accepted quorum",
+ );
+ assert_eq!(
+ farm_value["errors"][0]["detail"]["source"],
+ "SDK farm publish · local key"
+ );
+ assert_eq!(
+ farm_value["errors"][0]["detail"]["farm"]["target_relays"][0],
+ relay
+ );
+ assert_eq!(
+ farm_value["errors"][0]["detail"]["farm"]["failed_relays"][0]["relay"],
+ relay
+ );
+ assert_no_removed_command_reference(&farm_value, &["farm", "publish"]);
+ assert_no_daemon_runtime_reference(&farm_value, &["farm", "publish"]);
let listing_file = create_listing_draft(&sandbox, "direct-relay-attempt");
make_listing_publishable(&listing_file, farm_d_tag);
diff --git a/tests/target_cli.rs b/tests/target_cli.rs
@@ -3,21 +3,19 @@ mod support;
use std::fs;
use std::net::{TcpListener, TcpStream};
use std::path::Path;
-use std::sync::mpsc::{self, Receiver};
use std::thread::{self, JoinHandle};
-use std::time::Duration;
use radroots_events::RadrootsNostrEventPtr;
use radroots_events::ids::{
RadrootsInventoryBinId, RadrootsListingAddress, RadrootsOrderId, RadrootsPublicKey,
};
-use radroots_events::kinds::{KIND_FARM, KIND_ORDER_REQUEST, KIND_PROFILE};
+use radroots_events::kinds::KIND_ORDER_REQUEST;
use radroots_events::order::{RadrootsOrderEconomics, RadrootsOrderItem, RadrootsOrderRequest};
use radroots_events_codec::order::order_request_event_build;
use radroots_local_events::{
- BUYER_ORDER_REQUEST_LOCAL_WORK_RECORD_KIND, CANONICAL_RELAY_SET_FINGERPRINT_VERSION,
- LocalEventRecordInput, LocalEventsStore, LocalRecordFamily, LocalRecordStatus,
- PublishOutboxStatus, RelayDeliveryEvidence, SourceRuntime, canonical_relay_set_fingerprint,
+ BUYER_ORDER_REQUEST_LOCAL_WORK_RECORD_KIND, LocalEventRecordInput, LocalEventsStore,
+ LocalRecordFamily, LocalRecordStatus, PublishOutboxStatus, RelayDeliveryEvidence,
+ SourceRuntime, canonical_relay_set_fingerprint,
};
use radroots_nostr::prelude::{RadrootsNostrEvent, radroots_nostr_build_event};
use radroots_replica_db::{farm, migrations};
@@ -57,48 +55,6 @@ fn test_pubkey(value: &str) -> RadrootsPublicKey {
value.parse().expect("valid public key")
}
-struct RelayPublishServer {
- endpoint: String,
- requests: Receiver<Value>,
- handle: JoinHandle<()>,
-}
-
-impl RelayPublishServer {
- fn with_publish_outcomes(outcomes: Vec<(bool, &'static str)>) -> Self {
- let listener = TcpListener::bind("127.0.0.1:0").expect("bind relay");
- let endpoint = format!("ws://{}", listener.local_addr().expect("relay addr"));
- let (tx, requests) = mpsc::channel();
- let handle = thread::spawn(move || {
- for (accepted, reason) in outcomes {
- let (stream, _) = listener.accept().expect("accept relay connection");
- handle_relay_publish_connection(stream, accepted, reason, &tx);
- }
- });
-
- Self {
- endpoint,
- requests,
- handle,
- }
- }
-
- fn endpoint(&self) -> &str {
- self.endpoint.as_str()
- }
-
- fn take_requests(self, count: usize) -> Vec<Value> {
- let requests = (0..count)
- .map(|_| {
- self.requests
- .recv_timeout(Duration::from_secs(5))
- .expect("relay publish request")
- })
- .collect::<Vec<_>>();
- self.handle.join().expect("relay server join");
- requests
- }
-}
-
struct RelayFetchServer {
endpoint: String,
handle: JoinHandle<()>,
@@ -159,37 +115,6 @@ fn read_relay_req_subscription_id(websocket: &mut tungstenite::WebSocket<TcpStre
}
}
-fn handle_relay_publish_connection(
- stream: TcpStream,
- accepted: bool,
- reason: &str,
- tx: &mpsc::Sender<Value>,
-) {
- let mut websocket = tungstenite::accept(stream).expect("accept websocket");
- let event = read_relay_event_message(&mut websocket);
- let event_id = event["id"].as_str().expect("event id").to_owned();
- tx.send(event).expect("relay request send");
- websocket
- .send(tungstenite::Message::Text(
- json!(["OK", event_id, accepted, reason]).to_string().into(),
- ))
- .expect("relay ok send");
-}
-
-fn read_relay_event_message(websocket: &mut tungstenite::WebSocket<TcpStream>) -> Value {
- loop {
- let message = websocket.read().expect("relay message");
- if !message.is_text() {
- continue;
- }
- let value: Value =
- serde_json::from_str(message.to_text().expect("relay text")).expect("relay json");
- if value.get(0).and_then(Value::as_str) == Some("EVENT") {
- return value.get(1).cloned().expect("relay event payload");
- }
- }
-}
-
fn seed_legacy_replica_sync_farm(sandbox: &RadrootsCliSandbox, d_tag: &str, pubkey: &str) {
let executor = SqliteExecutor::open(sandbox.replica_db_path()).expect("open replica");
migrations::run_all_up(&executor).expect("replica migrations");
@@ -5602,10 +5527,10 @@ fn order_app_records_fail_closed_when_order_id_conflicts() {
}
#[test]
-fn farm_publish_writes_acknowledged_signed_outbox_records() {
+fn farm_publish_uses_sdk_outbox_without_legacy_signed_event_records() {
let sandbox = RadrootsCliSandbox::new();
sandbox.json_success(&["--format", "json", "account", "create"]);
- let farm = sandbox.json_success(&[
+ sandbox.json_success(&[
"--format",
"json",
"farm",
@@ -5619,84 +5544,47 @@ fn farm_publish_writes_acknowledged_signed_outbox_records() {
"--delivery-method",
"pickup",
]);
- let farm_config = &farm["result"]["config"];
- let farm_d_tag = farm_config["farm_d_tag"].as_str().expect("farm d tag");
- let account_id = farm_config["seller_account_id"]
- .as_str()
- .expect("seller account id");
- let seller_pubkey = farm_config["seller_pubkey"]
- .as_str()
- .expect("seller pubkey");
- let relay = RelayPublishServer::with_publish_outcomes(vec![(true, ""), (true, "")]);
- let relay_url = relay.endpoint().to_owned();
+ let relay_url = "ws://127.0.0.1:9";
+ let local_event_records_before_publish = sandbox.local_event_records().len();
- let publish = sandbox.json_success(&[
+ let (output, publish) = sandbox.json_output(&[
"--format",
"json",
"--relay",
- relay_url.as_str(),
+ relay_url,
"--approval-token",
"approve",
"farm",
"publish",
]);
+ assert!(!output.status.success());
assert_eq!(publish["operation_id"], "farm.publish");
- assert_eq!(publish["result"]["state"], "published");
- let profile_event_id = publish["result"]["profile"]["event_id"]
- .as_str()
- .expect("profile event id");
- let farm_event_id = publish["result"]["farm"]["event_id"]
- .as_str()
- .expect("farm event id");
- let requests = relay.take_requests(2);
- assert_eq!(requests.len(), 2);
+ assert_eq!(publish["result"], serde_json::Value::Null);
+ assert_eq!(publish["errors"][0]["code"], "network_unavailable");
+ let detail = &publish["errors"][0]["detail"];
+ assert_eq!(detail["source"], "SDK farm publish · local key");
+ assert_eq!(detail["state"], "unavailable");
+ assert_eq!(detail["profile"]["state"], "not_submitted");
+ assert_eq!(detail["profile"]["event_id"], serde_json::Value::Null);
+ assert_eq!(detail["farm"]["state"], "unavailable");
+ assert_eq!(detail["farm"]["target_relays"][0], relay_url);
+ assert_eq!(detail["farm"]["failed_relays"][0]["relay"], relay_url);
+ assert_eq!(
+ detail["farm"]["event_id"]
+ .as_str()
+ .expect("sdk farm event id")
+ .len(),
+ 64
+ );
let records = sandbox.local_event_records();
+ assert_eq!(records.len(), local_event_records_before_publish);
let signed_records = records
.iter()
.filter(|record| record.family == LocalRecordFamily::SignedEvent)
.collect::<Vec<_>>();
- assert_eq!(signed_records.len(), 2);
- for record in &signed_records {
- assert_eq!(record.status, LocalRecordStatus::Published);
- assert_eq!(record.outbox_status, PublishOutboxStatus::Acknowledged);
- assert_eq!(record.source_runtime, SourceRuntime::Cli);
- assert_eq!(record.owner_account_id.as_deref(), Some(account_id));
- assert_eq!(record.owner_pubkey.as_deref(), Some(seller_pubkey));
- assert_eq!(record.farm_id.as_deref(), Some(farm_d_tag));
- assert_eq!(
- record.relay_delivery_json.as_ref().unwrap()["state"],
- "acknowledged"
- );
- assert_eq!(
- record.relay_delivery_json.as_ref().unwrap()["acknowledged_relays"][0],
- relay_url
- );
- assert_eq!(
- record.relay_set_fingerprint.as_deref(),
- canonical_relay_set_fingerprint([relay_url.as_str()]).as_deref()
- );
- assert!(
- record
- .relay_set_fingerprint
- .as_deref()
- .expect("relay set fingerprint")
- .starts_with(CANONICAL_RELAY_SET_FINGERPRINT_VERSION)
- );
- assert_eq!(
- record.raw_event_json.as_ref().unwrap()["id"],
- record.event_id.as_deref().expect("event id")
- );
- }
- assert!(signed_records.iter().any(|record| {
- record.event_id.as_deref() == Some(profile_event_id)
- && record.event_kind == Some(i64::from(KIND_PROFILE))
- }));
- assert!(signed_records.iter().any(|record| {
- record.event_id.as_deref() == Some(farm_event_id)
- && record.event_kind == Some(i64::from(KIND_FARM))
- }));
+ assert!(signed_records.is_empty());
}
#[test]