cli

Command-line interface for Radroots
git clone https://radroots.dev/git/cli.git
Log | Files | Refs | README | LICENSE

commit 88cfcaefee7634ff971ef2cfd103917940f15231
parent 4174864280914916c5f126eba18bdeddacb878fc
Author: triesap <tyson@radroots.org>
Date:   Mon,  3 Aug 2026 14:37:24 +0000

cli: migrate product commands to final sdk api

- replace predecessor product wrappers with contextual farm listing and trade planners
- consume canonical storage status integrity backup and restore contracts
- translate native SDK error reports through stable protocol classes and recovery actions
- keep signer and synchronization composition explicit for the following atomic checkpoints

Diffstat:
MCargo.lock | 5+++++
MCargo.toml | 2++
Msrc/ops/error.rs | 91++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
Msrc/runtime/farm.rs | 613+++++++++++--------------------------------------------------------------------
Msrc/runtime/farm_config.rs | 34+++++++++++++++-------------------
Msrc/runtime/listing.rs | 652++++++++++++++++---------------------------------------------------------------
Msrc/runtime/sdk.rs | 64+++++++++++++++++++++++++++++++++++++++++++---------------------
Msrc/runtime/signer.rs | 2+-
Msrc/runtime/store.rs | 438++++++++++++++++++++++++++++++++++---------------------------------------------
Msrc/runtime/sync.rs | 33+++++++++++++++------------------
Msrc/runtime/trade.rs | 473++++++++++++++++++++++++++++++++++++-------------------------------------------
Msrc/runtime/transport.rs | 2+-
Msrc/view/runtime.rs | 104++++++++++++++++----------------------------------------------------------------
13 files changed, 773 insertions(+), 1740 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -2139,7 +2139,9 @@ dependencies = [ "radroots_runtime_store", "radroots_sdk", "radroots_secret_vault", + "radroots_signing", "radroots_sql_core", + "radroots_storage", "radroots_trade", "radroots_transport", "radroots_transport_nostr", @@ -2489,6 +2491,7 @@ dependencies = [ "radroots_event", "radroots_identity", "radroots_protocol", + "serde", ] [[package]] @@ -2511,6 +2514,7 @@ dependencies = [ "radroots_protocol", "radroots_trade", "radroots_transport", + "serde", ] [[package]] @@ -2561,6 +2565,7 @@ dependencies = [ "radroots_event", "radroots_identity", "radroots_protocol", + "serde", "sha2", ] diff --git a/Cargo.toml b/Cargo.toml @@ -49,7 +49,9 @@ radroots_runtime_contract_v1 = "=0.1.0-alpha" radroots_runtime_paths = "=0.1.0-alpha" radroots_sdk = { version = "=0.1.0-alpha", features = ["full"] } radroots_secret_vault = { version = "=0.1.0-alpha", features = ["std", "os-keyring"] } +radroots_signing = { version = "=0.1.0-alpha", features = ["std"] } radroots_sql_core = { version = "=0.1.0-alpha", features = ["native"] } +radroots_storage = "=0.1.0-alpha" radroots_trade = "=0.1.0-alpha" radroots_protocol = "=0.1.0-alpha" serde = { version = "1.0", features = ["derive"] } diff --git a/src/ops/error.rs b/src/ops/error.rs @@ -1,6 +1,7 @@ use std::io::ErrorKind; -use radroots_sdk::{RadrootsSdkError, RadrootsSdkErrorClass, RadrootsSdkRecoveryAction}; +use radroots_protocol::error::v1::{Class as ErrorClass, RecoveryAction}; +use radroots_sdk::Error as SdkError; use serde_json::{Map, Value, json}; use crate::out::envelope::{CliExitCode, OutputError}; @@ -355,13 +356,14 @@ impl OperationAdapterError { } } - pub fn sdk_failure(operation_id: &str, error: RadrootsSdkError) -> Self { - let code = error.code().to_owned(); - let class = sdk_error_class_name(error.class()).to_owned(); - let message = error.to_string(); - let exit_code = sdk_error_exit_code(error.class()); - let mut detail = error.detail_json(); - let actions = sdk_recovery_next_actions(operation_id, &error.recovery_actions()); + pub fn sdk_failure(operation_id: &str, error: SdkError) -> Self { + let report = error.to_report(); + let code = report.code().as_str().to_owned(); + let class = sdk_error_class_name(report.class()).to_owned(); + let message = report.message().as_str().to_owned(); + let exit_code = sdk_error_exit_code(report.class()); + let mut detail = serde_json::to_value(&report).unwrap_or_else(|_| json!({})); + let actions = sdk_recovery_next_actions(operation_id, report.recovery_actions()); if !actions.is_empty() && let Some(detail) = detail.as_object_mut() { @@ -560,57 +562,74 @@ impl OperationAdapterError { } } -fn sdk_error_exit_code(class: RadrootsSdkErrorClass) -> CliExitCode { +fn sdk_error_exit_code(class: ErrorClass) -> CliExitCode { match class { - RadrootsSdkErrorClass::Authorization => CliExitCode::AuthorizationFailed, - RadrootsSdkErrorClass::Clock - | RadrootsSdkErrorClass::Configuration - | RadrootsSdkErrorClass::Request => CliExitCode::InvalidInput, - RadrootsSdkErrorClass::Storage => CliExitCode::RuntimeUnavailable, - RadrootsSdkErrorClass::Transport => CliExitCode::SyncOrNetworkFailure, - RadrootsSdkErrorClass::Unsupported => CliExitCode::RuntimeUnavailable, + ErrorClass::Authorization | ErrorClass::Security => CliExitCode::AuthorizationFailed, + ErrorClass::Validation | ErrorClass::Contract | ErrorClass::Conflict => { + CliExitCode::InvalidInput + } + ErrorClass::Storage + | ErrorClass::Resource + | ErrorClass::Capability + | ErrorClass::Maintenance => CliExitCode::RuntimeUnavailable, + ErrorClass::Network | ErrorClass::Sync => CliExitCode::SyncOrNetworkFailure, _ => CliExitCode::InternalError, } } -fn sdk_error_class_name(class: RadrootsSdkErrorClass) -> &'static str { +fn sdk_error_class_name(class: ErrorClass) -> &'static str { match class { - RadrootsSdkErrorClass::Authorization => "authorization", - RadrootsSdkErrorClass::Clock => "clock", - RadrootsSdkErrorClass::Configuration => "configuration", - RadrootsSdkErrorClass::Request => "request", - RadrootsSdkErrorClass::Storage => "storage", - RadrootsSdkErrorClass::Transport => "transport", - RadrootsSdkErrorClass::Unsupported => "unsupported", - _ => "internal", + ErrorClass::Validation => "validation", + ErrorClass::Contract => "contract", + ErrorClass::Storage => "storage", + ErrorClass::Resource => "resource", + ErrorClass::Conflict => "conflict", + ErrorClass::Operation => "operation", + ErrorClass::Authorization => "authorization", + ErrorClass::Signer => "signer", + ErrorClass::Network => "network", + ErrorClass::Sync => "sync", + ErrorClass::Runtime => "runtime", + ErrorClass::Projection => "projection", + ErrorClass::Query => "query", + ErrorClass::Capability => "capability", + ErrorClass::Privacy => "privacy", + ErrorClass::Security => "security", + ErrorClass::Maintenance => "maintenance", + ErrorClass::Internal => "internal", + ErrorClass::Unknown => "unknown", } } fn sdk_recovery_next_actions( operation_id: &str, - recovery_actions: &[RadrootsSdkRecoveryAction], + recovery_actions: &[RecoveryAction], ) -> Vec<String> { recovery_actions .iter() .filter_map(|action| match action { - RadrootsSdkRecoveryAction::RetryOperationWithSameIdempotencyKey - | RadrootsSdkRecoveryAction::FixRequest => Some(operation_retry_action(operation_id)), - RadrootsSdkRecoveryAction::InspectLocalStores => { - Some("radroots store inspect".to_owned()) + RecoveryAction::RetryOperationWithSameIdempotencyKey | RecoveryAction::FixRequest => { + Some(operation_retry_action(operation_id)) + } + RecoveryAction::InspectLocalStores => Some("radroots store inspect".to_owned()), + RecoveryAction::ConfigureStorage => Some("radroots store init".to_owned()), + RecoveryAction::InspectGeoNamesAsset | RecoveryAction::ConfigureGeoNamesCache => { + Some("radroots health inspect".to_owned()) } - RadrootsSdkRecoveryAction::ConfigureTransportTargets => { + RecoveryAction::ConfigureTransportTargets => { Some("radroots transport config inspect".to_owned()) } - RadrootsSdkRecoveryAction::SelectAuthorizedActor => { - Some("radroots account list".to_owned()) + RecoveryAction::ConfigureSigner => Some("radroots signer status".to_owned()), + RecoveryAction::SelectAuthorizedActor => Some("radroots account list".to_owned()), + RecoveryAction::CompleteSignerAuthentication => { + Some("radroots signer status".to_owned()) } - RadrootsSdkRecoveryAction::RetryAfterTransportFailure => { + RecoveryAction::RetryAfterTransportFailure | RecoveryAction::RetryGeoNamesDownload => { Some(operation_retry_action(operation_id)) } - RadrootsSdkRecoveryAction::EnableRequiredFeature => { + RecoveryAction::EnableRequiredFeature | RecoveryAction::RecreateClient => { Some("radroots health inspect".to_owned()) } - _ => None, }) .fold(Vec::new(), |mut actions, action| { if !actions.contains(&action) { diff --git a/src/runtime/farm.rs b/src/runtime/farm.rs @@ -1,29 +1,21 @@ use std::sync::atomic::{AtomicU64, Ordering}; use std::time::{SystemTime, UNIX_EPOCH}; -use radroots_authority::RadrootsActorContext; -use radroots_event::contract::RadrootsActorRole; -use radroots_event::farm::{RadrootsFarm, RadrootsFarmPublicLocation}; -use radroots_event::ids::RadrootsAddressableCoordinate; -use radroots_event::kinds::{KIND_FARM, KIND_PROFILE}; -use radroots_event::operational_listing::RadrootsOperationalListingPublicLocation; -use radroots_event::profile::{RadrootsAuthoredProfile, RadrootsNip05Identifier}; +use radroots_event::contract::AuthorRole; +use radroots_event::envelope::kind::{KIND_FARM, KIND_PROFILE}; +use radroots_event::farm::{Farm, FarmPublicLocation}; +use radroots_event::id::AddressableCoordinate; +use radroots_event::listing::operational::OperationalListingPublicLocation; +use radroots_event::profile::{AuthoredProfile, Nip05Identifier}; use radroots_event_codec::d_tag::is_d_tag_base64url; use radroots_event_codec::profile::authored::authored_profile_to_wire_parts; -use radroots_sdk::{ - FarmEnqueuePublishRequest, FarmEnqueueReceipt, FarmPreparePublishRequest, - FarmPrivateLocationClearRequest, FarmPrivateLocationInput, FarmPrivateLocationLookupCandidate, - FarmPrivateLocationReceipt, FarmPrivateLocationSetRequest, FarmPrivateLocationSetResult, - FarmPublishPlan, GeocoderLocalityQuery, PushOutboxEventReceipt, PushOutboxEventState, - PushOutboxReceipt, PushOutboxRequest, PushOutboxTargetOutcomeKind, SdkExactLocation, - SdkMutationState, -}; +use radroots_sdk::farm::{self as sdk_farm, Plan as FarmPlan}; +use radroots_signing::{Actor, actor::ActorSource}; use serde_json::json; use crate::cli::global::{ FarmCreateArgs, FarmFieldArg, FarmPrivateLocationKeyArgs, FarmPrivateLocationSetArgs, - FarmPrivateLocationSetInput, FarmPublishArgs, FarmRebindArgs, FarmScopeArg, FarmScopedArgs, - FarmUpdateArgs, + FarmPublishArgs, FarmRebindArgs, FarmScopeArg, FarmScopedArgs, FarmUpdateArgs, }; use crate::runtime::RuntimeError; use crate::runtime::account::{self, AccountRecordView}; @@ -34,17 +26,13 @@ use crate::runtime::farm_config::{ SUPPORTED_FARM_CONFIG_VERSION, }; use crate::runtime::runtime_store::append_local_work; -use crate::runtime::sdk::{ - CliSdkAdapterError, CliSdkSession, sdk_nostr_relay_url_policy, sdk_target_outcome_kind_label, - sdk_target_policy, sdk_transport_outcome_kind_label, validate_configured_signer_for_actor, -}; +use crate::runtime::sdk::{CliSdkAdapterError, validate_configured_signer_for_actor}; use crate::runtime::signer::ActorWriteBindingError; use crate::view::runtime::{ FarmConfigDocumentView, FarmConfigSummaryView, FarmGetView, FarmListingDefaultsView, - FarmPrivateExactLocationView, FarmPrivateLocationCandidateView, FarmPrivateLocationView, - FarmPrivatePublicLocalityView, FarmProfileDraftView, FarmPublicationView, - FarmPublishComponentView, FarmPublishEventView, FarmPublishView, FarmRebindView, - FarmSelectionView, FarmSetView, FarmSetupView, FarmStatusView, TransportTargetFailureView, + FarmPrivateLocationView, FarmProfileDraftView, FarmPublicationView, FarmPublishComponentView, + FarmPublishEventView, FarmPublishView, FarmRebindView, FarmSelectionView, FarmSetView, + FarmSetupView, FarmStatusView, }; const FARM_CONFIG_SOURCE: &str = "farm config ยท local first"; @@ -347,46 +335,12 @@ pub fn private_location_set( config: &RuntimeConfig, args: &FarmPrivateLocationSetArgs, ) -> Result<FarmPrivateLocationView, CliSdkAdapterError> { - let target = match private_location_target(config, args.farm_d_tag.as_deref())? { - Some(target) => target, - None => return Ok(private_location_unconfigured_view()), - }; - let session = CliSdkSession::connect(config)?; - let geonames_status = session.sdk().geonames().ensure()?; - let mut request = FarmPrivateLocationSetRequest::new( - target.actor.clone(), - target.farm_d_tag.clone(), - sdk_private_location_input(&args.input), - ); - if let Some(label) = args.label.clone() { - request = request.with_label(label); - } - let geonames_database_path = Some(geonames_status.path.display().to_string()); - match session.block_on(session.sdk().farms().set_private_location(request))? { - FarmPrivateLocationSetResult::Stored(receipt) => Ok(private_location_view_from_receipt( - "set", - target, - Some(receipt), - geonames_database_path, - None, - )), - FarmPrivateLocationSetResult::NoMatch(lookup) => Ok(private_location_view_from_lookup( - "no_match", - target, - lookup.candidates, - geonames_database_path, - Some("GeoNames lookup returned no matching locality".to_owned()), - )), - FarmPrivateLocationSetResult::Ambiguous(lookup) => Ok(private_location_view_from_lookup( - "ambiguous", - target, - lookup.candidates, - geonames_database_path, - Some( - "GeoNames lookup matched multiple localities; retry with --region, --country, or --geonames-id" - .to_owned(), - ), - )), + match private_location_target(config, args.farm_d_tag.as_deref())? { + Some(_) => Err(RuntimeError::Config( + "private farm location writes require a host-owned private artifact adapter".to_owned(), + ) + .into()), + None => Ok(private_location_unconfigured_view()), } } @@ -394,68 +348,27 @@ pub fn private_location_get( config: &RuntimeConfig, args: &FarmPrivateLocationKeyArgs, ) -> Result<FarmPrivateLocationView, CliSdkAdapterError> { - let target = match private_location_target(config, args.farm_d_tag.as_deref())? { - Some(target) => target, - None => return Ok(private_location_unconfigured_view()), - }; - let session = CliSdkSession::connect(config)?; - let receipt = session.block_on(session.sdk().farms().private_location(&target.farm_addr))?; - let state = if receipt.is_some() { - "ready" - } else { - "missing" - }; - let reason = receipt - .is_none() - .then(|| "no private exact farm location is stored for this farm".to_owned()); - Ok(private_location_view_from_receipt( - state, target, receipt, None, reason, - )) + match private_location_target(config, args.farm_d_tag.as_deref())? { + Some(_) => Err(RuntimeError::Config( + "private farm location reads require a host-owned private artifact adapter".to_owned(), + ) + .into()), + None => Ok(private_location_unconfigured_view()), + } } pub fn private_location_clear( config: &RuntimeConfig, args: &FarmPrivateLocationKeyArgs, ) -> Result<FarmPrivateLocationView, CliSdkAdapterError> { - let target = match private_location_target(config, args.farm_d_tag.as_deref())? { - Some(target) => target, - None => return Ok(private_location_unconfigured_view()), - }; - let session = CliSdkSession::connect(config)?; - let request = - FarmPrivateLocationClearRequest::new(target.actor.clone(), target.farm_d_tag.clone()); - let receipt = session.block_on(session.sdk().farms().clear_private_location(request))?; - Ok(FarmPrivateLocationView { - state: if receipt.cleared { - "cleared" - } else { - "missing" - } - .to_owned(), - source: SDK_FARM_PRIVATE_LOCATION_SOURCE.to_owned(), - farm_addr: Some(receipt.farm_addr.to_string()), - farm_d_tag: Some(target.farm_d_tag), - seller_account_id: Some(target.seller_account_id), - seller_pubkey: Some(target.seller_pubkey), - label: None, - exact_location: None, - public_locality: None, - geonames_feature_id: None, - geonames_country_id: None, - geonames_database_path: None, - cleared: Some(receipt.cleared), - candidates: Vec::new(), - reason: (!receipt.cleared) - .then(|| "no private exact farm location was stored for this farm".to_owned()), - actions: if receipt.cleared { - Vec::new() - } else { - vec![ - "radroots farm location set --city <city> --region <region> --country <country>" - .to_owned(), - ] - }, - }) + match private_location_target(config, args.farm_d_tag.as_deref())? { + Some(_) => Err(RuntimeError::Config( + "private farm location deletion requires a host-owned private artifact adapter" + .to_owned(), + ) + .into()), + None => Ok(private_location_unconfigured_view()), + } } pub fn status( @@ -793,11 +706,12 @@ fn publish_via_sdk( }; } - let session = CliSdkSession::connect_memory(config)?; - let plan = session - .sdk() - .farms() - .prepare_publish(FarmPreparePublishRequest::new(input.actor, input.farm))?; + let plan = sdk_farm::prepare(sdk_farm::PrepareRequest::new( + input.actor, + input.farm, + now_unix(), + )) + .map_err(|error| RuntimeError::Config(format!("invalid SDK farm plan: {error}")))?; return Ok(sdk_prepared_publish_view( config, args, @@ -810,56 +724,22 @@ fn publish_via_sdk( )); } - let session = CliSdkSession::connect_for_actor( - config, - Some(resolved.document.selection.account.as_str()), - account_pubkey.as_str(), - "farm seller", - )?; - let mut request = - FarmEnqueuePublishRequest::new(input.actor, input.farm, sdk_target_policy(config)); - 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))?; - let push = session.block_on( - session.sdk().sync().push_outbox( - PushOutboxRequest::new() - .with_limit(1) - .with_nostr_relay_url_policy(sdk_nostr_relay_url_policy(config)), - ), - )?; - let view = sdk_enqueued_publish_view( - config, - args, - &resolved, - account_pubkey.as_str(), - previews, - profile_idempotency_key, - farm_idempotency_key, - &enqueue, - &push, - ); - if view.farm.state == "published" { - persist_farm_publication( - config, - &mut resolved, - enqueue.signed_event_id.as_str().to_owned(), - )?; - } - Ok(view) + Err(RuntimeError::Config( + "farm commit is unavailable until the shared sync engine is configured".to_owned(), + ) + .into()) } #[derive(Debug, Clone)] struct SdkFarmPublishInput { - actor: RadrootsActorContext, - farm: RadrootsFarm, + actor: Actor, + farm: Farm, } #[derive(Debug, Clone)] struct PrivateFarmLocationTarget { - actor: RadrootsActorContext, - farm_addr: RadrootsAddressableCoordinate, + actor: Actor, + farm_addr: AddressableCoordinate, farm_d_tag: String, seller_account_id: String, seller_pubkey: String, @@ -977,10 +857,8 @@ fn build_publish_previews( }) } -fn authored_profile_from_draft( - draft: &FarmProfileDraft, -) -> Result<RadrootsAuthoredProfile, RuntimeError> { - let mut profile = RadrootsAuthoredProfile::new(draft.name.clone()) +fn authored_profile_from_draft(draft: &FarmProfileDraft) -> Result<AuthoredProfile, RuntimeError> { + let mut profile = AuthoredProfile::new(draft.name.clone()) .map_err(|error| RuntimeError::Config(format!("invalid farm profile: {error}")))?; if let Some(display_name) = draft.display_name.as_deref().and_then(non_empty) { profile = profile.with_display_name(display_name); @@ -989,7 +867,7 @@ fn authored_profile_from_draft( profile = profile.with_about(about); } if let Some(nip05) = draft.nip05.as_deref().and_then(non_empty) { - let identifier = RadrootsNip05Identifier::parse(nip05.as_str()).map_err(|error| { + let identifier = Nip05Identifier::parse(nip05.as_str()).map_err(|error| { RuntimeError::Config(format!("invalid farm profile NIP-05 identifier: {error}")) })?; profile = profile.with_nip05(identifier); @@ -1011,7 +889,7 @@ fn authored_profile_from_draft( fn require_verified_publish_media( profile: &FarmProfileDraft, - farm: &RadrootsFarm, + farm: &Farm, ) -> Result<(), RuntimeError> { let contains_unverified_media = [ profile.picture.as_deref(), @@ -1171,10 +1049,10 @@ fn sdk_farm_publish_input( resolved: &ResolvedFarmConfig, account_pubkey: &str, ) -> Result<SdkFarmPublishInput, RuntimeError> { - let actor = RadrootsActorContext::local_account( + let actor = Actor::from_public_key_hex( account_pubkey, - resolved.document.selection.account.clone(), - [RadrootsActorRole::Farmer], + ActorSource::ExplicitPublicKey, + [AuthorRole::Farmer], ) .map_err(|error| RuntimeError::Config(format!("invalid farm SDK actor: {error}")))?; Ok(SdkFarmPublishInput { @@ -1195,7 +1073,7 @@ fn sdk_prepared_publish_view( previews: FarmPublishPreviews, profile_idempotency_key: Option<String>, farm_idempotency_key: Option<String>, - plan: FarmPublishPlan, + plan: FarmPlan, ) -> FarmPublishView { base_publish_view( "dry_run", @@ -1212,8 +1090,8 @@ fn sdk_prepared_publish_view( state: "not_submitted".to_owned(), reason: Some("dry run requested; SDK enqueue and transport 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_id: Some(plan.draft().expected_event_id().as_str().to_owned()), + event_addr: Some(plan.coordinate().as_str().to_owned()), event: args.print_event.then_some(sdk_plan_event_view(&plan)), ..preview_component( farm_publish_rpc_method(config), @@ -1228,184 +1106,17 @@ fn sdk_prepared_publish_view( ) } -#[expect( - clippy::too_many_arguments, - reason = "publish view construction mirrors the V1 output contract fields" -)] -fn sdk_enqueued_publish_view( - config: &RuntimeConfig, - args: &FarmPublishArgs, - 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_transport_endpoints: push_event - .map(sdk_push_target_transport_endpoints) - .unwrap_or_else(|| config.transport.nostr_relay_urls.clone()), - attempted_transport_endpoints: push_event - .map(sdk_push_attempted_transport_endpoints) - .unwrap_or_default(), - accepted_transport_endpoints: push_event - .map(sdk_push_accepted_transport_endpoints) - .unwrap_or_default(), - failed_transport_targets: push_event - .map(sdk_push_failed_transport_targets) - .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 sdk_plan_event_view(plan: &FarmPublishPlan) -> FarmPublishEventView { +fn sdk_plan_event_view(plan: &FarmPlan) -> FarmPublishEventView { FarmPublishEventView { - kind: plan.frozen_draft().kind_u32(), - author: plan.frozen_draft().expected_pubkey_str().to_owned(), - content: plan.frozen_draft().content().to_owned(), - tags: plan.frozen_draft().tags_as_vec(), - event_id: Some(plan.expected_event_id().as_str().to_owned()), - event_addr: Some(plan.farm_addr().as_str().to_owned()), + kind: plan.draft().kind_u32(), + author: plan.draft().expected_pubkey().to_hex(), + content: plan.draft().content().to_owned(), + tags: plan.draft().tags_as_vec(), + event_id: Some(plan.draft().expected_event_id().as_str().to_owned()), + event_addr: Some(plan.coordinate().as_str().to_owned()), } } -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" - } - Some(_) | None => "queued", - } - .to_owned() -} - -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 transport publish did not reach accepted quorum; outbox event remains retryable" - .to_owned(), - ), - Some(PushOutboxEventState::FailedTerminal) => { - Some("SDK transport publish failed terminally".to_owned()) - } - Some(state) => Some(format!( - "SDK transport push left event in state `{state:?}`" - )), - None => Some( - "farm publish queued in SDK outbox; no ready SDK outbox event was pushed".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_transport_endpoints(event: &PushOutboxEventReceipt) -> Vec<String> { - event - .targets - .iter() - .map(|target| target.endpoint_uri.clone()) - .collect() -} - -fn sdk_push_attempted_transport_endpoints(event: &PushOutboxEventReceipt) -> Vec<String> { - event - .targets - .iter() - .filter(|target| target.attempted) - .map(|target| target.endpoint_uri.clone()) - .collect() -} - -fn sdk_push_accepted_transport_endpoints(event: &PushOutboxEventReceipt) -> Vec<String> { - event - .targets - .iter() - .filter(|target| { - matches!( - target.outcome_kind, - PushOutboxTargetOutcomeKind::Accepted - | PushOutboxTargetOutcomeKind::DuplicateAccepted - ) - }) - .map(|target| target.endpoint_uri.clone()) - .collect() -} - -fn sdk_push_failed_transport_targets( - event: &PushOutboxEventReceipt, -) -> Vec<TransportTargetFailureView> { - event - .targets - .iter() - .filter(|target| { - !matches!( - target.outcome_kind, - PushOutboxTargetOutcomeKind::Accepted - | PushOutboxTargetOutcomeKind::DuplicateAccepted - ) - }) - .map(|target| TransportTargetFailureView { - transport_kind: target.transport_kind.clone(), - endpoint_uri: target.endpoint_uri.clone(), - target_scope: target.target_scope.clone(), - target_label: target.target_label.clone(), - transport_outcome_kind: target - .transport_outcome_kind - .map(sdk_transport_outcome_kind_label), - reason: target - .message - .clone() - .unwrap_or_else(|| sdk_target_outcome_kind_label(target.outcome_kind)), - }) - .collect() -} - fn profile_not_submitted_component( idempotency_key: Option<String>, args: &FarmPublishArgs, @@ -1423,33 +1134,6 @@ fn profile_not_submitted_component( } } -fn persist_farm_publication( - config: &RuntimeConfig, - resolved: &mut ResolvedFarmConfig, - event_id: String, -) -> Result<(), RuntimeError> { - persist_publication(config, resolved, None, Some(event_id)) -} - -fn persist_publication( - config: &RuntimeConfig, - resolved: &mut ResolvedFarmConfig, - profile_event_id: Option<String>, - farm_event_id: Option<String>, -) -> Result<(), RuntimeError> { - let published_at = now_unix(); - if let Some(event_id) = profile_event_id.and_then(|value| non_empty(value.as_str())) { - resolved.document.publication.profile_event_id = Some(event_id); - resolved.document.publication.profile_published_at = Some(published_at); - } - if let Some(event_id) = farm_event_id.and_then(|value| non_empty(value.as_str())) { - resolved.document.publication.farm_event_id = Some(event_id); - resolved.document.publication.farm_published_at = Some(published_at); - } - farm_config::write(&config.paths, resolved.scope, &resolved.document)?; - Ok(()) -} - fn farm_write_source(config: &RuntimeConfig) -> &'static str { let _ = config; SDK_FARM_WRITE_SOURCE @@ -1487,16 +1171,16 @@ fn private_location_target( let farm_d_tag = farm_d_tag .map(str::to_owned) .unwrap_or_else(|| resolved.document.selection.farm_d_tag.clone()); - let actor = RadrootsActorContext::local_account( + let actor = Actor::from_public_key_hex( seller_pubkey.as_str(), - seller_account_id.clone(), - [RadrootsActorRole::Farmer], + ActorSource::ExplicitPublicKey, + [AuthorRole::Farmer], ) .map_err(|error| { RuntimeError::Config(format!("invalid farm private location actor: {error}")) })?; let farm_addr = - RadrootsAddressableCoordinate::parse(format!("{KIND_FARM}:{seller_pubkey}:{farm_d_tag}")) + AddressableCoordinate::parse(format!("{KIND_FARM}:{seller_pubkey}:{farm_d_tag}")) .map_err(|error| RuntimeError::Config(format!("invalid farm address: {error}")))?; Ok(Some(PrivateFarmLocationTarget { actor, @@ -1541,134 +1225,6 @@ fn private_location_unconfigured_view() -> FarmPrivateLocationView { } } -fn private_location_view_from_receipt( - state: &str, - target: PrivateFarmLocationTarget, - receipt: Option<FarmPrivateLocationReceipt>, - geonames_database_path: Option<String>, - reason: Option<String>, -) -> FarmPrivateLocationView { - let receipt_ref = receipt.as_ref(); - FarmPrivateLocationView { - state: state.to_owned(), - source: SDK_FARM_PRIVATE_LOCATION_SOURCE.to_owned(), - farm_addr: Some(target.farm_addr.to_string()), - farm_d_tag: Some(target.farm_d_tag), - seller_account_id: Some(target.seller_account_id), - seller_pubkey: Some(target.seller_pubkey), - label: receipt_ref.and_then(|receipt| receipt.label.clone()), - exact_location: receipt_ref.map(|receipt| FarmPrivateExactLocationView { - lat: receipt.exact_location.latitude, - lng: receipt.exact_location.longitude, - }), - public_locality: receipt_ref.map(|receipt| FarmPrivatePublicLocalityView { - primary: receipt.public_locality.primary.clone(), - city: receipt.public_locality.city.clone(), - region: receipt.public_locality.region.clone(), - country: receipt.public_locality.country.clone(), - geohash5: receipt.public_locality.geohash5.clone(), - }), - geonames_feature_id: receipt_ref.and_then(|receipt| receipt.geonames_feature_id), - geonames_country_id: receipt_ref.and_then(|receipt| receipt.geonames_country_id.clone()), - geonames_database_path, - cleared: None, - candidates: Vec::new(), - reason, - actions: if receipt.is_some() { - Vec::new() - } else { - vec![ - "radroots farm location set --city <city> --region <region> --country <country>" - .to_owned(), - ] - }, - } -} - -fn private_location_view_from_lookup( - state: &str, - target: PrivateFarmLocationTarget, - candidates: Vec<FarmPrivateLocationLookupCandidate>, - geonames_database_path: Option<String>, - reason: Option<String>, -) -> FarmPrivateLocationView { - let actions = if state == "ambiguous" { - vec![ - "radroots farm location set --geonames-id <id>".to_owned(), - "radroots farm location set --city <city> --region <region> --country <country>" - .to_owned(), - ] - } else { - vec![ - "radroots farm location set --query \"<city>, <region>, <country>\"".to_owned(), - "radroots farm location set --lat <lat> --lng <lng>".to_owned(), - ] - }; - FarmPrivateLocationView { - state: state.to_owned(), - source: SDK_FARM_PRIVATE_LOCATION_SOURCE.to_owned(), - farm_addr: Some(target.farm_addr.to_string()), - farm_d_tag: Some(target.farm_d_tag), - seller_account_id: Some(target.seller_account_id), - seller_pubkey: Some(target.seller_pubkey), - label: None, - exact_location: None, - public_locality: None, - geonames_feature_id: None, - geonames_country_id: None, - geonames_database_path, - cleared: None, - candidates: private_location_candidate_views(candidates), - reason, - actions, - } -} - -fn sdk_private_location_input(input: &FarmPrivateLocationSetInput) -> FarmPrivateLocationInput { - match input { - FarmPrivateLocationSetInput::Exact { - latitude, - longitude, - } => FarmPrivateLocationInput::exact(SdkExactLocation::new(*latitude, *longitude)), - FarmPrivateLocationSetInput::City { - city, - region, - country, - } => { - let mut query = GeocoderLocalityQuery::structured(city.clone()); - if let Some(region) = region { - query = query.with_region(region.clone()); - } - if let Some(country) = country { - query = query.with_country(country.clone()); - } - FarmPrivateLocationInput::Locality(query) - } - FarmPrivateLocationSetInput::Query(query) => FarmPrivateLocationInput::query(query.clone()), - FarmPrivateLocationSetInput::GeonamesId(id) => FarmPrivateLocationInput::geonames_id(*id), - } -} - -fn private_location_candidate_views( - candidates: Vec<FarmPrivateLocationLookupCandidate>, -) -> Vec<FarmPrivateLocationCandidateView> { - candidates - .into_iter() - .map(|candidate| FarmPrivateLocationCandidateView { - geonames_feature_id: candidate.geonames_feature_id, - geonames_country_id: candidate.geonames_country_id, - name: candidate.name, - display_name: candidate.display_name, - exact_location: FarmPrivateExactLocationView { - lat: candidate.exact_location.latitude, - lng: candidate.exact_location.longitude, - }, - region: candidate.region, - country: candidate.country, - }) - .collect() -} - fn init_document( scope: FarmConfigScope, account: &AccountRecordView, @@ -1763,14 +1319,14 @@ fn init_document( lud16: None, bot: None, }, - farm: RadrootsFarm { + farm: Farm { d_tag: farm_d_tag, name, about, website, picture, banner, - location: Some(RadrootsFarmPublicLocation { + location: Some(FarmPublicLocation { primary: location_primary.clone(), city: city.clone(), region: region.clone(), @@ -1781,7 +1337,7 @@ fn init_document( }, listing_defaults: FarmListingDefaults { delivery_method, - location: RadrootsOperationalListingPublicLocation { + location: OperationalListingPublicLocation { primary: location_primary, city, region, @@ -2046,22 +1602,19 @@ fn apply_field_update( Ok(()) } -fn ensure_farm_location(document: &mut FarmConfigDocument) -> &mut RadrootsFarmPublicLocation { +fn ensure_farm_location(document: &mut FarmConfigDocument) -> &mut FarmPublicLocation { let primary = document.listing_defaults.location.primary.clone(); let city = document.listing_defaults.location.city.clone(); let region = document.listing_defaults.location.region.clone(); let country = document.listing_defaults.location.country.clone(); let geohash = document.listing_defaults.location.geohash.clone(); - document - .farm - .location - .get_or_insert(RadrootsFarmPublicLocation { - primary, - city, - region, - country, - geohash, - }) + document.farm.location.get_or_insert(FarmPublicLocation { + primary, + city, + region, + country, + geohash, + }) } fn publication_for_document( @@ -2378,7 +1931,7 @@ mod tests { use super::{authored_profile_from_draft, generate_d_tag, require_verified_publish_media}; use crate::runtime::RuntimeError; use crate::runtime::farm_config::FarmProfileDraft; - use radroots_event::farm::RadrootsFarm; + use radroots_event::farm::Farm; use radroots_event_codec::d_tag::is_d_tag_base64url; use radroots_event_codec::profile::authored::authored_profile_to_wire_parts; use serde_json::json; @@ -2448,8 +2001,8 @@ mod tests { assert_blossom_proof_error(require_verified_publish_media(&draft, &farm)); } - fn sample_farm() -> RadrootsFarm { - RadrootsFarm { + fn sample_farm() -> Farm { + Farm { d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_owned(), name: "Moss Street Farm".to_owned(), about: None, diff --git a/src/runtime/farm_config.rs b/src/runtime/farm_config.rs @@ -1,9 +1,9 @@ use std::fs; use std::path::{Path, PathBuf}; -use radroots_event::farm::RadrootsFarm; -use radroots_event::operational_listing::{ - RadrootsOperationalListingDeliveryMethod, RadrootsOperationalListingPublicLocation, +use radroots_event::farm::Farm; +use radroots_event::listing::operational::{ + OperationalListingDeliveryMethod, OperationalListingPublicLocation, }; use radroots_event_codec::d_tag::is_d_tag_base64url; use serde::{Deserialize, Serialize}; @@ -36,7 +36,7 @@ pub struct FarmConfigDocument { pub version: u32, pub selection: FarmConfigSelection, pub profile: FarmProfileDraft, - pub farm: RadrootsFarm, + pub farm: Farm, pub listing_defaults: FarmListingDefaults, #[serde(default)] pub publication: FarmPublicationStatus, @@ -74,13 +74,11 @@ pub struct FarmConfigSelection { #[serde(deny_unknown_fields)] pub struct FarmListingDefaults { pub delivery_method: String, - pub location: RadrootsOperationalListingPublicLocation, + pub location: OperationalListingPublicLocation, } impl FarmListingDefaults { - pub fn delivery_method_model( - &self, - ) -> Result<RadrootsOperationalListingDeliveryMethod, RuntimeError> { + pub fn delivery_method_model(&self) -> Result<OperationalListingDeliveryMethod, RuntimeError> { parse_delivery_method(self.delivery_method.as_str()) } } @@ -375,9 +373,7 @@ fn location_geohash(document: &FarmConfigDocument) -> Option<&str> { }) } -fn parse_delivery_method( - value: &str, -) -> Result<RadrootsOperationalListingDeliveryMethod, RuntimeError> { +fn parse_delivery_method(value: &str) -> Result<OperationalListingDeliveryMethod, RuntimeError> { let method = trimmed(value); if method.is_empty() { return Err(RuntimeError::Config( @@ -385,10 +381,10 @@ fn parse_delivery_method( )); } Ok(match method { - "pickup" => RadrootsOperationalListingDeliveryMethod::Pickup, - "local_delivery" => RadrootsOperationalListingDeliveryMethod::LocalDelivery, - "shipping" => RadrootsOperationalListingDeliveryMethod::Shipping, - other => RadrootsOperationalListingDeliveryMethod::Other { + "pickup" => OperationalListingDeliveryMethod::Pickup, + "local_delivery" => OperationalListingDeliveryMethod::LocalDelivery, + "shipping" => OperationalListingDeliveryMethod::Shipping, + other => OperationalListingDeliveryMethod::Other { method: other.to_owned(), }, }) @@ -413,7 +409,7 @@ mod tests { use std::path::PathBuf; - use radroots_event::farm::RadrootsFarmPublicLocation; + use radroots_event::farm::FarmPublicLocation; use tempfile::tempdir; fn sample_paths(profile: &str, root: &Path) -> PathsConfig { @@ -462,14 +458,14 @@ mod tests { lud16: None, bot: None, }, - farm: RadrootsFarm { + farm: Farm { d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_owned(), name: "La Huerta".to_owned(), about: Some("Small mixed vegetable farm.".to_owned()), website: Some("https://example.invalid/la-huerta".to_owned()), picture: None, banner: None, - location: Some(RadrootsFarmPublicLocation { + location: Some(FarmPublicLocation { primary: "San Francisco, CA".to_owned(), city: Some("San Francisco".to_owned()), region: Some("CA".to_owned()), @@ -480,7 +476,7 @@ mod tests { }, listing_defaults: FarmListingDefaults { delivery_method: "pickup".to_owned(), - location: RadrootsOperationalListingPublicLocation { + location: OperationalListingPublicLocation { primary: "San Francisco, CA".to_owned(), city: Some("San Francisco".to_owned()), region: Some("CA".to_owned()), diff --git a/src/runtime/listing.rs b/src/runtime/listing.rs @@ -4,32 +4,25 @@ use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicU64, Ordering}; use std::time::{SystemTime, UNIX_EPOCH}; -use radroots_authority::RadrootsActorContext; -use radroots_core::{ - RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreDiscount, RadrootsCoreDiscountScope, - RadrootsCoreDiscountThreshold, RadrootsCoreDiscountValue, RadrootsCoreMoney, - RadrootsCorePercent, RadrootsCoreQuantity, RadrootsCoreQuantityPrice, RadrootsCoreUnit, +use radroots_core::pricing::{Discount, DiscountScope, DiscountThreshold, DiscountValue}; +use radroots_core::{Currency, Decimal, Money, Percent, Quantity, QuantityPrice, Unit}; +use radroots_event::contract::AuthorRole; +use radroots_event::envelope::kind::KIND_CLASSIFIED_LISTING; +use radroots_event::farm::FarmRef; +use radroots_event::id::{DTag, InventoryBinId}; +use radroots_event::listing::operational::{ + OperationalListing, OperationalListingAvailability, OperationalListingBin, + OperationalListingDeliveryMethod, OperationalListingProduct, OperationalListingPublicLocation, + OperationalListingStatus, }; -use radroots_event::contract::RadrootsActorRole; -use radroots_event::farm::RadrootsFarmRef; -use radroots_event::ids::{RadrootsDTag, RadrootsInventoryBinId, RadrootsPublicKey}; -use radroots_event::kinds::KIND_CLASSIFIED_LISTING; -use radroots_event::operational_listing::{ - RadrootsOperationalListing, RadrootsOperationalListingAvailability, - RadrootsOperationalListingBin, RadrootsOperationalListingDeliveryMethod, - RadrootsOperationalListingProduct, RadrootsOperationalListingPublicLocation, - RadrootsOperationalListingStatus, -}; -use radroots_event::trade_validation::RadrootsOperationalListingValidationError; +use radroots_event::trade::validation::OperationalListingValidationError; use radroots_event_codec::d_tag::is_d_tag_base64url; use radroots_event_codec::operational_listing::encode::to_wire_parts_with_kind; +use radroots_identity::PublicKey; use radroots_replica_store::ReplicaSql; use radroots_runtime_store::{RuntimeStoreRecord, RuntimeStoreRecordFamily, SourceRuntime}; -use radroots_sdk::{ - ListingEnqueuePublishRequest, ListingEnqueueReceipt, ListingPreparePublishRequest, - ListingPublishPlan, PushOutboxEventReceipt, PushOutboxEventState, PushOutboxReceipt, - PushOutboxRequest, PushOutboxTargetOutcomeKind, SdkMutationState, -}; +use radroots_sdk::listing::{self as sdk_listing, Plan as ListingPlan}; +use radroots_signing::{Actor, actor::ActorSource}; use radroots_sql_core::SqlxSqliteExecutor; use radroots_trade::operational_listing::{ RadrootsOperationalListingEditDocumentV1, validate_operational_listing_model, @@ -49,10 +42,7 @@ use crate::runtime::runtime_store::{ append_local_work, get_shared_record, list_shared_records_before, list_shared_records_latest, shared_runtime_store_db_path, }; -use crate::runtime::sdk::{ - CliSdkAdapterError, CliSdkSession, sdk_nostr_relay_url_policy, sdk_target_outcome_kind_label, - sdk_target_policy, sdk_transport_outcome_kind_label, validate_configured_signer_for_actor, -}; +use crate::runtime::sdk::{CliSdkAdapterError, validate_configured_signer_for_actor}; use crate::runtime::sync::{ RelayIngestScope, freshness_for_scope_from_executor, market_refresh, missing_freshness, }; @@ -61,7 +51,6 @@ use crate::view::runtime::{ ListingAppRecordListView, ListingAppRecordSummaryView, ListingGetView, ListingListView, ListingMutationEventView, ListingMutationView, ListingNewView, ListingRebindView, ListingSummaryView, ListingValidateView, ListingValidationIssueView, MarketReadinessView, - TransportTargetFailureView, }; const DRAFT_KIND: &str = "listing_draft_v1"; @@ -78,16 +67,13 @@ const APP_RECORD_LIST_LIMIT: u32 = 500; static D_TAG_COUNTER: AtomicU64 = AtomicU64::new(0); -fn protocol_d_tag(value: &str, field: &str) -> Result<RadrootsDTag, RuntimeError> { +fn protocol_d_tag(value: &str, field: &str) -> Result<DTag, RuntimeError> { value .parse() .map_err(|error| RuntimeError::Config(format!("{field} is not a valid d tag: {error}"))) } -fn protocol_inventory_bin_id( - value: &str, - field: &str, -) -> Result<RadrootsInventoryBinId, RuntimeError> { +fn protocol_inventory_bin_id(value: &str, field: &str) -> Result<InventoryBinId, RuntimeError> { value.parse().map_err(|error| { RuntimeError::Config(format!("{field} is not a valid inventory bin id: {error}")) }) @@ -232,13 +218,13 @@ struct CanonicalListingDraft { seller_pubkey: String, seller_actor_source: String, farm_d_tag: String, - listing: RadrootsOperationalListing, + listing: OperationalListing, } #[derive(Debug, Clone)] struct SdkListingPublishInput { canonical: CanonicalListingDraft, - actor: RadrootsActorContext, + actor: Actor, document: RadrootsOperationalListingEditDocumentV1, } @@ -1536,8 +1522,8 @@ fn normalize_app_listing_availability(draft: &mut ListingDraftDocument) -> Resul fn normalize_app_listing_units(draft: &mut ListingDraftDocument) { let quantity_unit = draft.primary_bin.quantity_unit.trim().to_owned(); let price_per_unit = draft.primary_bin.price_per_unit.trim().to_owned(); - let quantity_unit_supported = quantity_unit.parse::<RadrootsCoreUnit>().is_ok(); - let price_per_unit_supported = price_per_unit.parse::<RadrootsCoreUnit>().is_ok(); + let quantity_unit_supported = quantity_unit.parse::<Unit>().is_ok(); + let price_per_unit_supported = price_per_unit.parse::<Unit>().is_ok(); if quantity_unit_supported && price_per_unit_supported { return; } @@ -1733,59 +1719,25 @@ pub fn publish_via_sdk( args: &ListingMutationArgs, ) -> Result<ListingMutationView, CliSdkAdapterError> { let input = sdk_listing_publish_input(config, args)?; - if config.output.dry_run { - validate_configured_listing_signer(config, &input.canonical)?; - let session = CliSdkSession::connect_memory(config)?; - let plan = session.sdk().listings().prepare_publish( - ListingPreparePublishRequest::from_document( - input.actor.clone(), - input.document.clone(), - ), - )?; - return Ok(sdk_prepared_publish_view( - config, - args, - ListingMutationOperation::Publish, - &input.canonical, - plan, - )); - } - - let session = CliSdkSession::connect_for_actor( - config, - Some(input.canonical.seller_account_id.as_str()), - input.canonical.seller_pubkey.as_str(), - "listing seller", - )?; - let mut request = ListingEnqueuePublishRequest::from_document( + validate_configured_listing_signer(config, &input.canonical)?; + let plan = sdk_listing::prepare(sdk_listing::PrepareRequest::publish( input.actor, input.document, - sdk_target_policy(config), - ); - if let Some(idempotency_key) = args.idempotency_key.as_deref() { - request = request.try_with_idempotency_key(idempotency_key)?; - } - let enqueue_receipt = session.block_on(session.sdk().listings().enqueue_publish(request))?; - let push_receipt = if args.offline { - None - } else { - Some( - session.block_on( - session.sdk().sync().push_outbox( - PushOutboxRequest::new() - .with_limit(1) - .with_nostr_relay_url_policy(sdk_nostr_relay_url_policy(config)), - ), - )?, + sdk_created_at_unix()?, + )) + .map_err(|error| RuntimeError::Config(format!("invalid SDK listing plan: {error}")))?; + if !config.output.dry_run { + return Err(RuntimeError::Config( + "listing commit is unavailable until the shared sync engine is configured".to_owned(), ) - }; - Ok(sdk_enqueued_publish_view( + .into()); + } + Ok(sdk_prepared_publish_view( config, args, ListingMutationOperation::Publish, &input.canonical, - enqueue_receipt, - push_receipt, + plan, )) } @@ -1824,10 +1776,10 @@ fn sdk_listing_publish_input( )) })?; ensure_listing_bound_account(config, &canonical, args.file.as_path())?; - let actor = RadrootsActorContext::local_account( + let actor = Actor::from_public_key_hex( canonical.seller_pubkey.as_str(), - canonical.seller_account_id.clone(), - [RadrootsActorRole::Seller], + ActorSource::ExplicitPublicKey, + [AuthorRole::Seller], ) .map_err(|error| RuntimeError::Config(format!("invalid listing SDK actor: {error}")))?; let document = RadrootsOperationalListingEditDocumentV1::new(canonical.listing.clone()); @@ -1843,9 +1795,9 @@ fn sdk_prepared_publish_view( args: &ListingMutationArgs, operation: ListingMutationOperation, canonical: &CanonicalListingDraft, - plan: ListingPublishPlan, + plan: ListingPlan, ) -> ListingMutationView { - let listing_addr = plan.public_listing_addr().as_str().to_owned(); + let listing_addr = plan.address().as_str().to_owned(); let event = sdk_plan_event_view(&plan); ListingMutationView { state: "dry_run".to_owned(), @@ -1867,7 +1819,7 @@ fn sdk_prepared_publish_view( job_id: None, job_status: None, signer_mode: Some(config.signer.backend.as_str().to_owned()), - event_id: Some(plan.expected_event_id().as_str().to_owned()), + event_id: Some(plan.draft().expected_event_id().as_str().to_owned()), event_addr: Some(listing_addr), idempotency_key: args.idempotency_key.clone(), local_replica: None, @@ -1878,203 +1830,19 @@ fn sdk_prepared_publish_view( } } -fn sdk_enqueued_publish_view( - config: &RuntimeConfig, - args: &ListingMutationArgs, - operation: ListingMutationOperation, - canonical: &CanonicalListingDraft, - enqueue: ListingEnqueueReceipt, - push: Option<PushOutboxReceipt>, -) -> ListingMutationView { - let push_event = push - .as_ref() - .and_then(|receipt| sdk_push_event_for_listing(&enqueue, receipt)); - let state = sdk_publish_state(args, push_event); - let reason = sdk_publish_reason(args, push_event); - let target_transport_endpoints = push_event - .map(sdk_push_target_transport_endpoints) - .unwrap_or_else(|| config.transport.nostr_relay_urls.clone()); - let attempted_transport_endpoints = push_event - .map(sdk_push_attempted_transport_endpoints) - .unwrap_or_default(); - let accepted_transport_endpoints = push_event - .map(sdk_push_accepted_transport_endpoints) - .unwrap_or_default(); - let failed_transport_targets = push_event - .map(sdk_push_failed_transport_targets) - .unwrap_or_default(); - let event_id = enqueue.signed_event_id.as_str().to_owned(); - let listing_addr = enqueue.public_listing_addr.as_str().to_owned(); - ListingMutationView { - state, - operation: operation.as_str().to_owned(), - source: SDK_LISTING_WRITE_SOURCE.to_owned(), - file: args.file.display().to_string(), - listing_id: canonical.listing_id.clone(), - listing_addr: listing_addr.clone(), - seller_account_id: canonical.seller_account_id.clone(), - seller_pubkey: canonical.seller_pubkey.clone(), - seller_actor_source: canonical.seller_actor_source.clone(), - event_kind: KIND_CLASSIFIED_LISTING, - dry_run: false, - deduplicated: matches!(enqueue.state, SdkMutationState::AlreadyQueued), - target_transport_endpoints, - attempted_transport_endpoints, - accepted_transport_endpoints, - failed_transport_targets, - job_id: None, - job_status: None, - signer_mode: Some(config.signer.backend.as_str().to_owned()), - event_id: Some(event_id), - event_addr: Some(listing_addr), - idempotency_key: args.idempotency_key.clone(), - local_replica: None, - reason, - job: None, - event: None, - actions: sdk_publish_actions(args, push_event), - } -} - -fn sdk_plan_event_view(plan: &ListingPublishPlan) -> ListingMutationEventView { +fn sdk_plan_event_view(plan: &ListingPlan) -> ListingMutationEventView { ListingMutationEventView { - kind: plan.frozen_draft().kind_u32(), - author: plan.frozen_draft().expected_pubkey_str().to_owned(), - created_at: Some(plan.frozen_draft().created_at_u64()), - content: plan.frozen_draft().content().to_owned(), - tags: plan.frozen_draft().tags_as_vec(), - event_id: Some(plan.expected_event_id().as_str().to_owned()), + kind: plan.draft().kind_u32(), + author: plan.draft().expected_pubkey().to_hex(), + created_at: Some(plan.draft().created_at_u64()), + content: plan.draft().content().to_owned(), + tags: plan.draft().tags_as_vec(), + event_id: Some(plan.draft().expected_event_id().as_str().to_owned()), signature: None, - event_addr: plan.public_listing_addr().as_str().to_owned(), - } -} - -fn sdk_push_event_for_listing<'a>( - enqueue: &ListingEnqueueReceipt, - push: &'a PushOutboxReceipt, -) -> Option<&'a PushOutboxEventReceipt> { - push.events - .iter() - .find(|event| event.event_id == enqueue.signed_event_id) -} - -fn sdk_publish_state( - args: &ListingMutationArgs, - push_event: Option<&PushOutboxEventReceipt>, -) -> String { - match push_event.map(|event| event.final_state) { - Some(PushOutboxEventState::Published) => "published", - Some(PushOutboxEventState::PublishRetryable | PushOutboxEventState::FailedTerminal) => { - "unavailable" - } - Some(_) | None if args.offline => "queued", - Some(_) | None => "queued", - } - .to_owned() -} - -fn sdk_publish_reason( - args: &ListingMutationArgs, - push_event: Option<&PushOutboxEventReceipt>, -) -> Option<String> { - match push_event.map(|event| event.final_state) { - Some(PushOutboxEventState::Published) => None, - Some(PushOutboxEventState::PublishRetryable) => Some( - "SDK transport publish did not reach accepted quorum; outbox event remains retryable" - .to_owned(), - ), - Some(PushOutboxEventState::FailedTerminal) => { - Some("SDK transport publish failed terminally".to_owned()) - } - Some(state) => Some(format!( - "SDK transport push left event in state `{state:?}`" - )), - None if args.offline => Some( - "listing publish queued in SDK outbox; transport push skipped for offline mode" - .to_owned(), - ), - None => Some( - "listing publish queued in SDK outbox; no ready SDK outbox event was pushed".to_owned(), - ), + event_addr: plan.address().as_str().to_owned(), } } -fn sdk_publish_actions( - args: &ListingMutationArgs, - push_event: Option<&PushOutboxEventReceipt>, -) -> Vec<String> { - if args.offline - || !matches!( - push_event.map(|event| event.final_state), - Some(PushOutboxEventState::Published) - ) - { - return vec!["radroots sync push".to_owned()]; - } - Vec::new() -} - -fn sdk_push_target_transport_endpoints(event: &PushOutboxEventReceipt) -> Vec<String> { - event - .targets - .iter() - .map(|target| target.endpoint_uri.clone()) - .collect() -} - -fn sdk_push_attempted_transport_endpoints(event: &PushOutboxEventReceipt) -> Vec<String> { - event - .targets - .iter() - .filter(|target| target.attempted) - .map(|target| target.endpoint_uri.clone()) - .collect() -} - -fn sdk_push_accepted_transport_endpoints(event: &PushOutboxEventReceipt) -> Vec<String> { - event - .targets - .iter() - .filter(|target| { - matches!( - target.outcome_kind, - PushOutboxTargetOutcomeKind::Accepted - | PushOutboxTargetOutcomeKind::DuplicateAccepted - ) - }) - .map(|target| target.endpoint_uri.clone()) - .collect() -} - -fn sdk_push_failed_transport_targets( - event: &PushOutboxEventReceipt, -) -> Vec<TransportTargetFailureView> { - event - .targets - .iter() - .filter(|target| { - !matches!( - target.outcome_kind, - PushOutboxTargetOutcomeKind::Accepted - | PushOutboxTargetOutcomeKind::DuplicateAccepted - ) - }) - .map(|target| TransportTargetFailureView { - transport_kind: target.transport_kind.clone(), - endpoint_uri: target.endpoint_uri.clone(), - target_scope: target.target_scope.clone(), - target_label: target.target_label.clone(), - transport_outcome_kind: target - .transport_outcome_kind - .map(sdk_transport_outcome_kind_label), - reason: target - .message - .clone() - .unwrap_or_else(|| sdk_target_outcome_kind_label(target.outcome_kind)), - }) - .collect() -} - pub fn update( config: &RuntimeConfig, args: &ListingMutationArgs, @@ -2134,8 +1902,8 @@ fn mutate( ensure_listing_bound_account(config, &canonical, args.file.as_path())?; if let Some(status) = operation.listing_status() { - canonical.listing.availability = Some(RadrootsOperationalListingAvailability::Status { - status: RadrootsOperationalListingStatus::Other { + canonical.listing.availability = Some(OperationalListingAvailability::Status { + status: OperationalListingStatus::Other { value: status.to_owned(), }, }); @@ -2154,59 +1922,38 @@ fn mutate_via_sdk_from_canonical( operation: ListingMutationOperation, canonical: CanonicalListingDraft, ) -> Result<ListingMutationView, CliSdkAdapterError> { - let actor = RadrootsActorContext::local_account( + let actor = Actor::from_public_key_hex( canonical.seller_pubkey.as_str(), - canonical.seller_account_id.clone(), - [RadrootsActorRole::Seller], + ActorSource::ExplicitPublicKey, + [AuthorRole::Seller], ) .map_err(|error| RuntimeError::Config(format!("invalid listing SDK actor: {error}")))?; let document = RadrootsOperationalListingEditDocumentV1::new(canonical.listing.clone()); - if config.output.dry_run { - let session = CliSdkSession::connect_memory(config)?; - let plan = session - .sdk() - .listings() - .prepare_publish(ListingPreparePublishRequest::from_document(actor, document))?; - return Ok(sdk_prepared_publish_view( - config, args, operation, &canonical, plan, - )); - } - - let session = CliSdkSession::connect_for_actor( - config, - Some(canonical.seller_account_id.as_str()), - canonical.seller_pubkey.as_str(), - "listing seller", - )?; - let mut request = - ListingEnqueuePublishRequest::from_document(actor, document, sdk_target_policy(config)); - if let Some(idempotency_key) = args.idempotency_key.as_deref() { - request = request.try_with_idempotency_key(idempotency_key)?; - } - let enqueue_receipt = session.block_on(session.sdk().listings().enqueue_publish(request))?; - let push_receipt = if args.offline { - None + let request = if matches!(operation, ListingMutationOperation::Publish) { + sdk_listing::PrepareRequest::publish(actor, document, sdk_created_at_unix()?) } else { - Some( - session.block_on( - session.sdk().sync().push_outbox( - PushOutboxRequest::new() - .with_limit(1) - .with_nostr_relay_url_policy(sdk_nostr_relay_url_policy(config)), - ), - )?, - ) + sdk_listing::PrepareRequest::update(actor, document, sdk_created_at_unix()?) }; - Ok(sdk_enqueued_publish_view( - config, - args, - operation, - &canonical, - enqueue_receipt, - push_receipt, + let plan = sdk_listing::prepare(request) + .map_err(|error| RuntimeError::Config(format!("invalid SDK listing plan: {error}")))?; + if !config.output.dry_run { + return Err(RuntimeError::Config( + "listing commit is unavailable until the shared sync engine is configured".to_owned(), + ) + .into()); + } + Ok(sdk_prepared_publish_view( + config, args, operation, &canonical, plan, )) } +fn sdk_created_at_unix() -> Result<u64, RuntimeError> { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|duration| duration.as_secs()) + .map_err(|error| RuntimeError::Config(format!("system clock error: {error}"))) +} + fn scaffold_contents(draft: &ListingDraftDocument) -> Result<String, RuntimeError> { let toml = toml::to_string_pretty(draft).map_err(|error| { RuntimeError::Config(format!("failed to render listing draft: {error}")) @@ -2336,7 +2083,7 @@ fn canonicalize_draft( contents, "primary_bin.quantity_unit", )?; - let quantity = RadrootsCoreQuantity::new(quantity_amount, quantity_unit) + let quantity = Quantity::new(quantity_amount, quantity_unit) .with_optional_label(non_empty(draft.primary_bin.label.clone())) .to_canonical() .map_err(|error| { @@ -2367,9 +2114,9 @@ fn canonicalize_draft( contents, "primary_bin.price_per_unit", )?; - let price = RadrootsCoreQuantityPrice::new( - RadrootsCoreMoney::new(price_amount, price_currency), - RadrootsCoreQuantity::new(price_per_amount, price_per_unit), + let price = QuantityPrice::new( + Money::new(price_amount, price_currency), + Quantity::new(price_per_amount, price_per_unit), ) .try_to_canonical_unit_price() .map_err(|error| { @@ -2405,7 +2152,7 @@ fn canonicalize_draft( }, )?; - let listing = RadrootsOperationalListing { + let listing = OperationalListing { d_tag: protocol_d_tag(listing_id.as_str(), "listing d_tag").map_err(|error| { issue_for_field( contents, @@ -2414,11 +2161,11 @@ fn canonicalize_draft( ) })?, published_at: None, - farm: RadrootsFarmRef { + farm: FarmRef { pubkey: seller_pubkey.clone(), d_tag: farm_d_tag.clone(), }, - product: RadrootsOperationalListingProduct { + product: OperationalListingProduct { key: draft.product.key.trim().to_owned(), title: draft.product.title.trim().to_owned(), category: draft.product.category.trim().to_owned(), @@ -2430,7 +2177,7 @@ fn canonicalize_draft( year: None, }, primary_bin_id: primary_bin_id.clone(), - bins: vec![RadrootsOperationalListingBin { + bins: vec![OperationalListingBin { bin_id: primary_bin_id, quantity, price_per_canonical_unit: price, @@ -2463,7 +2210,7 @@ fn canonicalize_draft( fn build_availability( draft: &ListingDraftDocument, contents: &str, -) -> Result<RadrootsOperationalListingAvailability, ListingValidationIssueView> { +) -> Result<OperationalListingAvailability, ListingValidationIssueView> { let kind = if draft.availability.kind.trim().is_empty() { if draft.availability.start.is_some() || draft.availability.end.is_some() { "window" @@ -2484,17 +2231,17 @@ fn build_availability( "missing availability status", )); } - Ok(RadrootsOperationalListingAvailability::Status { + Ok(OperationalListingAvailability::Status { status: match status { - "active" => RadrootsOperationalListingStatus::Active, - "sold" => RadrootsOperationalListingStatus::Sold, - other => RadrootsOperationalListingStatus::Other { + "active" => OperationalListingStatus::Active, + "sold" => OperationalListingStatus::Sold, + other => OperationalListingStatus::Other { value: other.to_owned(), }, }, }) } - "window" => Ok(RadrootsOperationalListingAvailability::Window { + "window" => Ok(OperationalListingAvailability::Window { start: draft.availability.start, end: draft.availability.end, }), @@ -2509,7 +2256,7 @@ fn build_availability( fn build_delivery_method( draft: &ListingDraftDocument, contents: &str, -) -> Result<RadrootsOperationalListingDeliveryMethod, ListingValidationIssueView> { +) -> Result<OperationalListingDeliveryMethod, ListingValidationIssueView> { let method = draft.delivery.method.trim(); if method.is_empty() { return Err(issue_for_field( @@ -2520,17 +2267,17 @@ fn build_delivery_method( } Ok(match method { - "pickup" => RadrootsOperationalListingDeliveryMethod::Pickup, - "local_delivery" => RadrootsOperationalListingDeliveryMethod::LocalDelivery, - "shipping" => RadrootsOperationalListingDeliveryMethod::Shipping, - other => RadrootsOperationalListingDeliveryMethod::Other { + "pickup" => OperationalListingDeliveryMethod::Pickup, + "local_delivery" => OperationalListingDeliveryMethod::LocalDelivery, + "shipping" => OperationalListingDeliveryMethod::Shipping, + other => OperationalListingDeliveryMethod::Other { method: other.to_owned(), }, }) } -fn build_location(draft: &ListingDraftDocument) -> RadrootsOperationalListingPublicLocation { - RadrootsOperationalListingPublicLocation { +fn build_location(draft: &ListingDraftDocument) -> OperationalListingPublicLocation { + OperationalListingPublicLocation { primary: draft.location.primary.trim().to_owned(), city: draft.location.city.clone().and_then(non_empty), region: draft.location.region.clone().and_then(non_empty), @@ -2543,8 +2290,8 @@ fn build_listing_discounts( draft: &ListingDraftDocument, contents: &str, primary_bin_id: &str, - price_currency: RadrootsCoreCurrency, -) -> Result<Option<Vec<RadrootsCoreDiscount>>, ListingValidationIssueView> { + price_currency: Currency, +) -> Result<Option<Vec<Discount>>, ListingValidationIssueView> { let mut discounts = Vec::new(); for (index, discount) in draft.discounts.iter().enumerate() { let field_prefix = format!("discounts.{index}"); @@ -2580,14 +2327,14 @@ fn build_listing_discounts( "percent discount requires value", )); } - let percent = raw.parse::<RadrootsCorePercent>().map_err(|error| { + let percent = raw.parse::<Percent>().map_err(|error| { issue_for_field( contents, field_prefix.as_str(), format!("percent discount value is invalid: {error}"), ) })?; - RadrootsCoreDiscountValue::Percent(percent) + DiscountValue::Percent(percent) } "amount" => { let raw_amount = discount.amount.trim(); @@ -2608,7 +2355,7 @@ fn build_listing_discounts( field_prefix.as_str(), )? }; - RadrootsCoreDiscountValue::MoneyPerBin(RadrootsCoreMoney::new(amount, currency)) + DiscountValue::MoneyPerBin(Money::new(amount, currency)) } other => { return Err(issue_for_field( @@ -2618,9 +2365,9 @@ fn build_listing_discounts( )); } }; - let discount = RadrootsCoreDiscount { - scope: RadrootsCoreDiscountScope::Bin, - threshold: RadrootsCoreDiscountThreshold::BinCount { bin_id, min }, + let discount = Discount { + scope: DiscountScope::Bin, + threshold: DiscountThreshold::BinCount { bin_id, min }, value, }; if !discount.is_non_negative() { @@ -2795,60 +2542,60 @@ fn validate_configured_listing_signer( fn validate_operational_listing_draft( canonical: &CanonicalListingDraft, -) -> Result<(), RadrootsOperationalListingValidationError> { - let seller_pubkey = RadrootsPublicKey::parse(canonical.seller_pubkey.as_str()) - .map_err(|_| RadrootsOperationalListingValidationError::InvalidSeller)?; +) -> Result<(), OperationalListingValidationError> { + let seller_pubkey = PublicKey::parse(canonical.seller_pubkey.as_str()) + .map_err(|_| OperationalListingValidationError::InvalidSeller)?; validate_operational_listing_model(canonical.listing.clone(), &seller_pubkey).map(|_| ()) } fn issue_from_trade_validation( - error: RadrootsOperationalListingValidationError, + error: OperationalListingValidationError, contents: &str, ) -> ListingValidationIssueView { match error { - RadrootsOperationalListingValidationError::InvalidSeller => issue_for_field( + OperationalListingValidationError::InvalidSeller => issue_for_field( contents, "seller_actor.pubkey", "listing author does not match the farm pubkey", ), - RadrootsOperationalListingValidationError::MissingTitle => { + OperationalListingValidationError::MissingTitle => { issue_for_field(contents, "product.title", "missing listing title") } - RadrootsOperationalListingValidationError::MissingDescription => { + OperationalListingValidationError::MissingDescription => { issue_for_field(contents, "product.summary", "missing listing description") } - RadrootsOperationalListingValidationError::MissingProductType => { + OperationalListingValidationError::MissingProductType => { issue_for_field(contents, "product.category", "missing listing product type") } - RadrootsOperationalListingValidationError::MissingBins - | RadrootsOperationalListingValidationError::MissingPrimaryBin - | RadrootsOperationalListingValidationError::InvalidBin => { + OperationalListingValidationError::MissingBins + | OperationalListingValidationError::MissingPrimaryBin + | OperationalListingValidationError::InvalidBin => { issue_for_field(contents, "primary_bin.bin_id", error.to_string()) } - RadrootsOperationalListingValidationError::MissingPrice - | RadrootsOperationalListingValidationError::InvalidPrice => issue_for_field( + OperationalListingValidationError::MissingPrice + | OperationalListingValidationError::InvalidPrice => issue_for_field( contents, "primary_bin.price_amount", "invalid listing price", ), - RadrootsOperationalListingValidationError::MissingInventory - | RadrootsOperationalListingValidationError::InvalidInventory => { + OperationalListingValidationError::MissingInventory + | OperationalListingValidationError::InvalidInventory => { issue_for_field(contents, "inventory.available", error.to_string()) } - RadrootsOperationalListingValidationError::MissingAvailability => issue_for_field( + OperationalListingValidationError::MissingAvailability => issue_for_field( contents, "availability.status", "missing listing availability", ), - RadrootsOperationalListingValidationError::MissingLocation - | RadrootsOperationalListingValidationError::MissingLocationLocality => { + OperationalListingValidationError::MissingLocation + | OperationalListingValidationError::MissingLocationLocality => { issue_for_field(contents, "location.primary", error.to_string()) } - RadrootsOperationalListingValidationError::MissingLocationGeohash - | RadrootsOperationalListingValidationError::InvalidLocationGeohash => { + OperationalListingValidationError::MissingLocationGeohash + | OperationalListingValidationError::InvalidLocationGeohash => { issue_for_field(contents, "location.geohash", error.to_string()) } - RadrootsOperationalListingValidationError::MissingDeliveryMethod => issue_for_field( + OperationalListingValidationError::MissingDeliveryMethod => issue_for_field( contents, "delivery.method", "missing listing delivery method", @@ -2951,9 +2698,7 @@ fn authoring_defaults(config: &RuntimeConfig) -> Result<ListingAuthoringDefaults Ok(defaults) } -fn draft_location_from_model( - location: &RadrootsOperationalListingPublicLocation, -) -> ListingDraftLocation { +fn draft_location_from_model(location: &OperationalListingPublicLocation) -> ListingDraftLocation { ListingDraftLocation { primary: location.primary.clone(), city: location.city.clone(), @@ -3001,8 +2746,8 @@ fn parse_decimal_field( value: &str, contents: &str, field: &str, -) -> Result<RadrootsCoreDecimal, ListingValidationIssueView> { - value.trim().parse::<RadrootsCoreDecimal>().map_err(|_| { +) -> Result<Decimal, ListingValidationIssueView> { + value.trim().parse::<Decimal>().map_err(|_| { issue_for_field( contents, field, @@ -3015,8 +2760,8 @@ fn parse_unit_field( value: &str, contents: &str, field: &str, -) -> Result<RadrootsCoreUnit, ListingValidationIssueView> { - value.parse::<RadrootsCoreUnit>().map_err(|_| { +) -> Result<Unit, ListingValidationIssueView> { + value.parse::<Unit>().map_err(|_| { issue_for_field( contents, field, @@ -3029,9 +2774,9 @@ fn parse_currency_field( value: &str, contents: &str, field: &str, -) -> Result<RadrootsCoreCurrency, ListingValidationIssueView> { +) -> Result<Currency, ListingValidationIssueView> { let upper = value.trim().to_ascii_uppercase(); - RadrootsCoreCurrency::from_str_upper(&upper).map_err(|_| { + Currency::from_str_upper(&upper).map_err(|_| { issue_for_field( contents, field, @@ -3148,18 +2893,8 @@ fn encode_base64url_no_pad(bytes: [u8; 16]) -> String { #[cfg(test)] mod tests { - use super::{ - DRAFT_KIND, ListingDraftDocument, encode_base64url_no_pad, generate_d_tag, - sdk_publish_actions, sdk_publish_reason, sdk_publish_state, - sdk_push_accepted_transport_endpoints, sdk_push_failed_transport_targets, - }; - use crate::cli::global::ListingMutationArgs; - use radroots_event::ids::RadrootsEventId; + use super::{DRAFT_KIND, ListingDraftDocument, encode_base64url_no_pad, generate_d_tag}; use radroots_event_codec::d_tag::is_d_tag_base64url; - use radroots_sdk::{ - PushOutboxEventReceipt, PushOutboxEventState, PushOutboxTargetOutcomeKind, - PushOutboxTargetReceipt, PushOutboxTransportOutcomeKind, - }; #[test] fn generated_listing_d_tag_is_valid_base64url() { @@ -3175,49 +2910,6 @@ mod tests { } #[test] - fn sdk_push_receipt_helpers_map_published_and_auth_required_states() { - let accepted = sdk_push_event( - PushOutboxEventState::Published, - PushOutboxTargetOutcomeKind::Accepted, - Some("accepted".to_owned()), - ); - let args = listing_mutation_args(false); - - assert_eq!(sdk_publish_state(&args, Some(&accepted)), "published"); - assert!(sdk_publish_reason(&args, Some(&accepted)).is_none()); - assert!(sdk_publish_actions(&args, Some(&accepted)).is_empty()); - assert_eq!( - sdk_push_accepted_transport_endpoints(&accepted), - vec!["ws://127.0.0.1:19000".to_owned()] - ); - assert!(sdk_push_failed_transport_targets(&accepted).is_empty()); - - let auth_required = sdk_push_event( - PushOutboxEventState::PublishRetryable, - PushOutboxTargetOutcomeKind::AuthRequired, - Some("auth required".to_owned()), - ); - let failed = sdk_push_failed_transport_targets(&auth_required); - - assert_eq!( - sdk_publish_state(&args, Some(&auth_required)), - "unavailable" - ); - assert!( - sdk_publish_reason(&args, Some(&auth_required)) - .expect("retry reason") - .contains("accepted quorum") - ); - assert_eq!(failed.len(), 1); - assert_eq!(failed[0].endpoint_uri, "ws://127.0.0.1:19000"); - assert_eq!(failed[0].reason, "auth required"); - assert_eq!( - sdk_publish_actions(&args, Some(&auth_required)), - vec!["radroots sync push".to_owned()] - ); - } - - #[test] fn listing_draft_kind_constant_is_stable() { let document = ListingDraftDocument { version: 1, @@ -3358,89 +3050,7 @@ mod tests { missing_description.listing.product.summary = Some(" ".to_owned()); assert_eq!( super::validate_operational_listing_draft(&missing_description), - Err( - radroots_event::trade_validation::RadrootsOperationalListingValidationError::MissingDescription - ) + Err(super::OperationalListingValidationError::MissingDescription) ); } - - fn sdk_push_event( - final_state: PushOutboxEventState, - outcome_kind: PushOutboxTargetOutcomeKind, - message: Option<String>, - ) -> PushOutboxEventReceipt { - PushOutboxEventReceipt { - event_id: RadrootsEventId::parse("e".repeat(64)).expect("event id"), - outbox_event_id: 7, - final_state, - attempted_count: 1, - accepted_count: usize::from(matches!( - outcome_kind, - PushOutboxTargetOutcomeKind::Accepted - | PushOutboxTargetOutcomeKind::DuplicateAccepted - )), - retryable_count: usize::from(matches!( - outcome_kind, - PushOutboxTargetOutcomeKind::AuthRequired - | PushOutboxTargetOutcomeKind::Timeout - | PushOutboxTargetOutcomeKind::ConnectionFailed - )), - terminal_count: 0, - quorum: 1, - quorum_met: matches!( - outcome_kind, - PushOutboxTargetOutcomeKind::Accepted - | PushOutboxTargetOutcomeKind::DuplicateAccepted - ), - targets: vec![PushOutboxTargetReceipt { - transport_kind: "nostr".to_owned(), - endpoint_uri: "ws://127.0.0.1:19000".to_owned(), - target_scope: None, - target_label: None, - outcome_kind, - transport_outcome_kind: test_transport_outcome_kind(outcome_kind), - attempted: true, - message, - }], - } - } - - fn test_transport_outcome_kind( - kind: PushOutboxTargetOutcomeKind, - ) -> Option<PushOutboxTransportOutcomeKind> { - Some(match kind { - PushOutboxTargetOutcomeKind::Accepted => PushOutboxTransportOutcomeKind::Accepted, - PushOutboxTargetOutcomeKind::DuplicateAccepted => { - PushOutboxTransportOutcomeKind::DuplicateAccepted - } - PushOutboxTargetOutcomeKind::Timeout => PushOutboxTransportOutcomeKind::Timeout, - PushOutboxTargetOutcomeKind::ConnectionFailed => { - PushOutboxTransportOutcomeKind::ConnectionFailed - } - PushOutboxTargetOutcomeKind::AuthRequired - | PushOutboxTargetOutcomeKind::Blocked - | PushOutboxTargetOutcomeKind::RateLimited - | PushOutboxTargetOutcomeKind::Invalid - | PushOutboxTargetOutcomeKind::PowRequired - | PushOutboxTargetOutcomeKind::Restricted - | PushOutboxTargetOutcomeKind::Muted - | PushOutboxTargetOutcomeKind::Unsupported - | PushOutboxTargetOutcomeKind::PaymentRequired - | PushOutboxTargetOutcomeKind::Error - | PushOutboxTargetOutcomeKind::TargetUriRejected - | PushOutboxTargetOutcomeKind::SkippedAlreadyAccepted - | PushOutboxTargetOutcomeKind::DeferredUntilImplemented - | PushOutboxTargetOutcomeKind::Unknown => PushOutboxTransportOutcomeKind::Rejected, - _ => PushOutboxTransportOutcomeKind::Rejected, - }) - } - - fn listing_mutation_args(offline: bool) -> ListingMutationArgs { - ListingMutationArgs { - file: "listing.toml".into(), - idempotency_key: None, - print_event: false, - offline, - } - } } diff --git a/src/runtime/sdk.rs b/src/runtime/sdk.rs @@ -2,7 +2,7 @@ use std::fs; use std::future::Future; use std::path::PathBuf; use std::sync::Arc; -use std::time::Duration; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; use radroots_authority::RadrootsLocalEventSigner; use radroots_identity::RadrootsIdentity; @@ -16,9 +16,9 @@ use radroots_nostr_connect::prelude::{ RadrootsNostrConnectClientTarget, RadrootsNostrConnectError, RadrootsNostrConnectUri, }; use radroots_sdk::{ - MeshScopeId, MultiTargetProfile, NostrProfile, NostrRelayUrlPolicy, - PushOutboxTargetOutcomeKind, PushOutboxTransportOutcomeKind, RadrootsClient, - RadrootsClientBuilder, RadrootsSdkError, RadrootsSdkLocalKeySigner, + Client, ClientBuilder, Error as SdkError, MeshScopeId, MultiTargetProfile, NostrProfile, + NostrRelayUrlPolicy, PushOutboxTargetOutcomeKind, PushOutboxTransportOutcomeKind, + RadrootsClient, RadrootsClientBuilder, RadrootsSdkLocalKeySigner, RadrootsSdkMycNip46RequestPolicy, RadrootsSdkMycNip46Signer, RadrootsSdkNip46Transport, RadrootsSdkNip46TransportFuture, RadrootsSdkSignerProvider, RadrootsSdkStorageConfig, RadrootsdExecutionProfile, ReticulumAgentEndpoint, ReticulumBehavior as SdkReticulumBehavior, @@ -50,7 +50,7 @@ pub enum CliSdkAdapterError { #[error("{0}")] Runtime(#[from] RuntimeError), #[error("{0}")] - Sdk(#[from] RadrootsSdkError), + Sdk(#[from] SdkError), } pub fn sdk_transport_outcome_kind_label(kind: PushOutboxTransportOutcomeKind) -> String { @@ -91,24 +91,44 @@ impl CliSdkConfig { } } - pub fn builder(&self) -> RadrootsClientBuilder { - let builder = RadrootsClient::builder() - .storage(RadrootsSdkStorageConfig::Directory( - self.storage_root.clone(), - )) - .geonames_cache_root(self.geonames_cache_root.clone()) - .transport_profile(self.transport_profile.clone()); - if let Some(profile) = self.radrootsd_execution_profile.clone() { - builder.radrootsd_execution_profile(profile) - } else { - builder + fn sqlite_options(&self) -> Result<radroots_sdk::storage::SqliteOptions, RuntimeError> { + fs::create_dir_all(&self.storage_root)?; + let paths = radroots_sdk::storage::SqlitePaths::from_directory(&self.storage_root) + .map_err(|error| RuntimeError::Config(format!("invalid SDK storage paths: {error}")))?; + let mut options = radroots_sdk::storage::SqliteOptions::new( + paths, + radroots_sdk::storage::SqliteOpenMode::Create, + ); + if !self.storage_root.join("runtime.sqlite").exists() { + let mut bytes = [0_u8; 32]; + getrandom::getrandom(&mut bytes).map_err(|error| { + RuntimeError::Config(format!("failed to generate SDK source identity: {error}")) + })?; + let generation = + radroots_storage::event::SourceGeneration::new(bytes).map_err(|error| { + RuntimeError::Config(format!("invalid SDK source identity: {error}")) + })?; + let created_at_unix_ms = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|error| RuntimeError::Config(format!("system clock error: {error}")))? + .as_millis() + .try_into() + .map_err(|_| { + RuntimeError::Config("system clock is outside SDK range".to_owned()) + })?; + options = options + .with_source_generation(generation, created_at_unix_ms) + .map_err(|error| { + RuntimeError::Config(format!("invalid SDK source identity: {error}")) + })?; } + Ok(options) } } pub struct CliSdkSession { runtime: Runtime, - sdk: RadrootsClient, + sdk: Client, config: CliSdkConfig, } @@ -116,7 +136,8 @@ impl CliSdkSession { pub fn connect(config: &RuntimeConfig) -> Result<Self, CliSdkAdapterError> { let sdk_config = CliSdkConfig::from_runtime_config(config)?; let runtime = sdk_runtime()?; - let sdk = runtime.block_on(sdk_config.builder().build())?; + let options = sdk_config.sqlite_options()?; + let sdk = runtime.block_on(ClientBuilder::sqlite(options))?.build()?; Ok(Self { runtime, sdk, @@ -127,7 +148,8 @@ impl CliSdkSession { pub fn connect_storage_status(config: &RuntimeConfig) -> Result<Self, CliSdkAdapterError> { let sdk_config = CliSdkConfig::from_runtime_config_for_storage_status(config); let runtime = sdk_runtime()?; - let sdk = runtime.block_on(sdk_config.builder().build())?; + let options = sdk_config.sqlite_options()?; + let sdk = runtime.block_on(ClientBuilder::sqlite(options))?.build()?; Ok(Self { runtime, sdk, @@ -138,7 +160,7 @@ impl CliSdkSession { pub fn connect_memory(config: &RuntimeConfig) -> Result<Self, CliSdkAdapterError> { let sdk_config = CliSdkConfig::from_runtime_config(config)?; let runtime = sdk_runtime()?; - let sdk = runtime.block_on(memory_builder(&sdk_config).build())?; + let sdk = ClientBuilder::memory_default().build()?; Ok(Self { runtime, sdk, @@ -193,7 +215,7 @@ impl CliSdkSession { }) } - pub fn sdk(&self) -> &RadrootsClient { + pub fn sdk(&self) -> &Client { &self.sdk } diff --git a/src/runtime/signer.rs b/src/runtime/signer.rs @@ -9,7 +9,7 @@ use crate::view::runtime::{ IdentityPublicView, LocalSignerStatusView, MycStatusView, SignerBindingStatusView, SignerStatusView, SignerWriteKindReadinessView, }; -use radroots_event::kinds::{ +use radroots_event::envelope::kind::{ KIND_CLASSIFIED_LISTING, KIND_FARM, KIND_TRADE_CANCELLATION, KIND_TRADE_DECISION, KIND_TRADE_PROPOSAL, KIND_TRADE_REVISION_DECISION, KIND_TRADE_REVISION_PROPOSAL, }; diff --git a/src/runtime/store.rs b/src/runtime/store.rs @@ -1,28 +1,28 @@ use std::fs; use std::path::{Path, PathBuf}; +use std::time::{SystemTime, UNIX_EPOCH}; use radroots_replica_store::export::{ReplicaStoreExportManifestRs, export_manifest}; use radroots_replica_store::migrations; use radroots_replica_sync::radroots_replica_sync_status; -use radroots_sdk::{ - BackupReceipt, BackupRequest, IntegrityReceipt, IntegrityRequest, RadrootsClient, - RestoreReceipt, RestoreRequest, SdkBackupState, SdkEventStoreStorageStatus, - SdkOutboxStorageStatus, SdkRestoreState, SdkSqliteStoreStatus, SdkStorageKind, - StorageStatusReceipt, StorageStatusRequest, -}; +use radroots_sdk::storage::{IntegrityStatus, Status as StorageStatus}; use radroots_sql_core::SqlxSqliteExecutor; +use radroots_storage::{ + backup::{BackupFormatVersion, BackupId, BackupPlan, BackupSecretPolicy, RestorePlan}, + status::{IntegrityHealth, ShutdownState, StorageBackend, StorageOpenMode, WriterPolicy}, +}; use serde::Serialize; use serde_json::{Value, json}; use crate::cli::global::LocalExportFormatArg; use crate::runtime::RuntimeError; use crate::runtime::config::RuntimeConfig; -use crate::runtime::sdk::{CliSdkAdapterError, CliSdkSession, sdk_runtime, sdk_storage_root}; +use crate::runtime::sdk::{CliSdkAdapterError, CliSdkSession, sdk_storage_root}; use crate::runtime::sync::ensure_sync_run_table; use crate::view::runtime::{ LocalBackupView, LocalDerivedProjectionStatusView, LocalExportView, LocalInitView, LocalReplicaCountsView, LocalReplicaSyncView, LocalRestoreView, LocalStatusView, - SdkEventStoreStatusView, SdkIntegrityView, SdkOutboxStatusView, SdkSqliteStatusView, + SdkIntegrityView, SdkStorageStatusView, }; const DERIVED_PROJECTION_SOURCE: &str = "local derived projection cache"; @@ -32,7 +32,6 @@ const SDK_BACKUP_KIND: &str = "sdk_canonical"; const SDK_BACKUP_MANIFEST_FILE: &str = "manifest.json"; const SDK_RUNTIME_FILE: &str = "runtime.sqlite"; const SDK_PRIVATE_FILE: &str = "private.sqlite"; -const SDK_STUDIO_FILE: &str = "studio.sqlite"; pub fn init(config: &RuntimeConfig) -> Result<LocalInitView, RuntimeError> { let existed = config.local.replica_store_path.exists(); @@ -90,8 +89,8 @@ pub fn status(config: &RuntimeConfig) -> Result<LocalStatusView, CliSdkAdapterEr let sdk_existed_before_open = sdk_storage_files_exist(sdk_root.as_path()); let derived_projection = derived_projection_status(config)?; let session = CliSdkSession::connect(config)?; - let receipt = session.block_on(session.sdk().storage_status(StorageStatusRequest::new()))?; - let integrity = session.block_on(session.sdk().integrity(IntegrityRequest::new()))?; + let receipt = session.block_on(session.sdk().storage_status())?; + let integrity = session.block_on(session.sdk().storage_integrity())?; Ok(sdk_status_view( config, sdk_root, @@ -159,8 +158,34 @@ pub fn backup( ) -> Result<LocalBackupView, CliSdkAdapterError> { ensure_safe_sdk_backup_destination(config, output)?; let session = CliSdkSession::connect(config)?; - let receipt = session.block_on(session.sdk().backup(BackupRequest::new(output)))?; - sdk_backup_view(receipt) + let plan = sdk_backup_plan()?; + let operation = session + .block_on(session.sdk().storage_operations()?.begin_backup(plan)) + .map_err(|error| RuntimeError::Config(format!("SDK backup planning failed: {error}")))?; + Ok(LocalBackupView { + state: format!("{:?}", operation.stage()).to_lowercase(), + source: SDK_CANONICAL_SOURCE.to_owned(), + backup_kind: SDK_BACKUP_KIND.to_owned(), + canonical_store: SDK_CANONICAL_STORE.to_owned(), + destination: output.display().to_string(), + file: output.join(SDK_BACKUP_MANIFEST_FILE).display().to_string(), + event_store_file: None, + outbox_file: None, + manifest_file: None, + size_bytes: 0, + manifest: json!({ + "backup_id": hex_bytes(operation.plan().backup_id().as_bytes()), + "format_version": operation.plan().format_version().get(), + "secret_policy": format!("{:?}", operation.plan().secret_policy()).to_lowercase(), + "requested_at_unix_ms": operation.plan().requested_at_unix_ms(), + "revision": operation.revision().get(), + }), + reason: Some( + "backup is durably planned; the host capture worker must complete staged capture, verification, and finalization" + .to_owned(), + ), + actions: vec!["radroots store status".to_owned()], + }) } pub fn backup_preflight( @@ -169,8 +194,8 @@ pub fn backup_preflight( ) -> Result<LocalBackupView, CliSdkAdapterError> { ensure_safe_sdk_backup_destination(config, output)?; let session = CliSdkSession::connect(config)?; - let status = session.block_on(session.sdk().storage_status(StorageStatusRequest::new()))?; - let integrity = session.block_on(session.sdk().integrity(IntegrityRequest::new()))?; + let status = session.block_on(session.sdk().storage_status())?; + let integrity = session.block_on(session.sdk().storage_integrity())?; let manifest = sdk_backup_manifest_preview(output, &status, &integrity); Ok(LocalBackupView { state: "dry_run".to_owned(), @@ -202,13 +227,50 @@ pub fn restore( .map(Path::to_path_buf) .unwrap_or_else(|| sdk_storage_root(config)); ensure_safe_sdk_restore_destination(config, &destination)?; - let request = RestoreRequest::new(source) - .with_destination(destination) - .with_overwrite(overwrite) - .with_dry_run(dry_run); - let runtime = sdk_runtime()?; - let receipt = runtime.block_on(RadrootsClient::restore(request))?; - sdk_restore_view(receipt, overwrite, dry_run) + let manifest_path = source.join(SDK_BACKUP_MANIFEST_FILE); + let manifest = serde_json::from_slice::<radroots_storage::backup::BackupManifest>(&fs::read( + &manifest_path, + )?) + .map_err(|error| RuntimeError::Config(format!("invalid backup manifest: {error}")))?; + let plan = RestorePlan::new(manifest.clone(), manifest.secret_policy(), unix_ms()?) + .map_err(|error| RuntimeError::Config(format!("invalid restore plan: {error}")))?; + let session = CliSdkSession::connect(config)?; + let operation = session + .block_on(session.sdk().storage_operations()?.begin_restore(plan)) + .map_err(|error| RuntimeError::Config(format!("SDK restore planning failed: {error}")))?; + if !dry_run { + return Err(RuntimeError::Config( + "restore is planned but requires the host staging and atomic replacement worker" + .to_owned(), + ) + .into()); + } + Ok(LocalRestoreView { + state: "dry_run".to_owned(), + source: SDK_CANONICAL_SOURCE.to_owned(), + restore_kind: SDK_BACKUP_KIND.to_owned(), + canonical_store: SDK_CANONICAL_STORE.to_owned(), + backup_source: source.display().to_string(), + destination: destination.display().to_string(), + event_store_file: source.join(SDK_RUNTIME_FILE).display().to_string(), + outbox_file: source.join(SDK_RUNTIME_FILE).display().to_string(), + manifest_file: manifest_path.display().to_string(), + destination_event_store_file: Some( + destination.join(SDK_RUNTIME_FILE).display().to_string(), + ), + destination_outbox_file: Some(destination.join(SDK_RUNTIME_FILE).display().to_string()), + restored_event_store_file: None, + restored_outbox_file: None, + manifest: json_value(&manifest)?, + verification: json!({ + "stage": format!("{:?}", operation.stage()).to_lowercase(), + "revision": operation.revision().get(), + }), + overwrite, + dry_run, + reason: Some("dry run requested; restore was validated and not staged".to_owned()), + actions: vec!["radroots store restore <backup-dir>".to_owned()], + }) } pub fn export( @@ -339,127 +401,109 @@ fn validate_directory_target(path: &Path) -> Result<(), RuntimeError> { } fn sdk_storage_files_exist(sdk_root: &Path) -> bool { - sdk_root.join(SDK_RUNTIME_FILE).exists() - && sdk_root.join(SDK_PRIVATE_FILE).exists() - && sdk_root.join(SDK_STUDIO_FILE).exists() + sdk_root.join(SDK_RUNTIME_FILE).exists() && sdk_root.join(SDK_PRIVATE_FILE).exists() } fn sdk_status_view( config: &RuntimeConfig, sdk_root: PathBuf, sdk_existed_before_open: bool, - receipt: StorageStatusReceipt, - integrity: IntegrityReceipt, + status: StorageStatus, + integrity: IntegrityStatus, derived_projection: LocalDerivedProjectionStatusView, ) -> LocalStatusView { - let runtime_path = receipt - .paths - .as_ref() - .map(|paths| paths.runtime_path.display().to_string()); - let state = sdk_status_state(&receipt, &integrity).to_owned(); - let reason = sdk_status_reason(&state); - let actions = sdk_status_actions(&state); + let state = if integrity.health() == IntegrityHealth::Healthy { + "ready" + } else { + "needs_attention" + }; LocalStatusView { - state, + state: state.to_owned(), source: SDK_CANONICAL_SOURCE.to_owned(), local_root: config.local.root.display().to_string(), canonical_store: SDK_CANONICAL_STORE.to_owned(), - sdk_storage: sdk_storage_kind_label(receipt.storage).to_owned(), + sdk_storage: storage_backend_label(status.backend()).to_owned(), sdk_root: sdk_root.display().to_string(), sdk_existed_before_open, - event_store: sdk_event_store_status_view(receipt.event_store, runtime_path.clone()), - outbox: sdk_outbox_status_view(receipt.outbox, runtime_path), - integrity: sdk_integrity_view(integrity), + storage: SdkStorageStatusView { + backend: storage_backend_label(status.backend()).to_owned(), + open_mode: storage_open_mode_label(status.open_mode()).to_owned(), + writer_policy: writer_policy_label(status.writer_policy()).to_owned(), + shutdown: shutdown_state_label(status.shutdown()).to_owned(), + wal_enabled: status.wal_enabled(), + busy_timeout_ms: status.busy_timeout_ms(), + }, + integrity: SdkIntegrityView { + health: integrity_health_label(integrity.health()).to_owned(), + checked_at_unix_ms: integrity.checked_at_unix_ms(), + verified_members: integrity.verified_members(), + failed_members: integrity.failed_members(), + }, derived_projection, - reason, - actions, - } -} - -fn sdk_status_state(receipt: &StorageStatusReceipt, integrity: &IntegrityReceipt) -> &'static str { - if receipt.event_store.store.integrity_ok - && receipt.outbox.store.integrity_ok - && integrity.event_store_ok - && integrity.outbox_ok - { - "ready" - } else { - "needs_attention" + reason: (state != "ready") + .then(|| "SDK canonical store integrity requires attention".to_owned()), + actions: if state == "ready" { + Vec::new() + } else { + vec!["radroots store inspect".to_owned()] + }, } } -fn sdk_status_reason(state: &str) -> Option<String> { - match state { - "ready" => None, - _ => Some("SDK canonical store integrity check failed".to_owned()), +fn storage_backend_label(value: StorageBackend) -> &'static str { + match value { + StorageBackend::Memory => "memory", + StorageBackend::Sqlite => "sqlite", } } -fn sdk_status_actions(state: &str) -> Vec<String> { - match state { - "ready" => Vec::new(), - _ => vec!["radroots store inspect".to_owned()], +fn storage_open_mode_label(value: StorageOpenMode) -> &'static str { + match value { + StorageOpenMode::ReadOnly => "read_only", + StorageOpenMode::ReadWriteExisting => "read_write_existing", + StorageOpenMode::Create => "create", } } -fn sdk_event_store_status_view( - status: SdkEventStoreStorageStatus, - path: Option<String>, -) -> SdkEventStoreStatusView { - SdkEventStoreStatusView { - path, - store: sdk_sqlite_status_view(status.store), - total_events: status.total_events, - valid_stream_events: status.valid_stream_events, - transport_observations: status.transport_observations, - last_event_seq: status.last_event_seq, - last_event_updated_at_ms: status.last_event_updated_at_ms, +fn writer_policy_label(value: WriterPolicy) -> &'static str { + match value { + WriterPolicy::NoWriter => "no_writer", + WriterPolicy::AdvisoryProcessLock => "advisory_process_lock", } } -fn sdk_outbox_status_view( - status: SdkOutboxStorageStatus, - path: Option<String>, -) -> SdkOutboxStatusView { - SdkOutboxStatusView { - path, - store: sdk_sqlite_status_view(status.store), - total_events: status.total_events, - pending_events: status.pending_events, - retryable_events: status.retryable_events, - terminal_events: status.terminal_events, - failed_terminal_events: status.failed_terminal_events, - deferred_until_implemented_events: status.deferred_until_implemented_events, - ready_signed_events: status.ready_signed_events, - publishing_events: status.publishing_events, - last_attempt_at_ms: status.last_attempt_at_ms, - last_error: status.last_error, +fn shutdown_state_label(value: ShutdownState) -> &'static str { + match value { + ShutdownState::Open => "open", + ShutdownState::Closing => "closing", + ShutdownState::Closed => "closed", } } -fn sdk_sqlite_status_view(status: SdkSqliteStoreStatus) -> SdkSqliteStatusView { - SdkSqliteStatusView { - schema_version: status.schema_version, - journal_mode: status.journal_mode, - foreign_keys_enabled: status.foreign_keys_enabled, - busy_timeout_ms: status.busy_timeout_ms, - integrity_ok: status.integrity_ok, - integrity_result: status.integrity_result, +fn integrity_health_label(value: IntegrityHealth) -> &'static str { + match value { + IntegrityHealth::Healthy => "healthy", + IntegrityHealth::Degraded => "degraded", + IntegrityHealth::Corrupt => "corrupt", + IntegrityHealth::Unknown => "unknown", } } -fn sdk_integrity_view(receipt: IntegrityReceipt) -> SdkIntegrityView { - SdkIntegrityView { - checked_paths: receipt - .checked_paths - .into_iter() - .map(|path| path.display().to_string()) - .collect(), - event_store_ok: receipt.event_store_ok, - outbox_ok: receipt.outbox_ok, - event_store_result: receipt.event_store_result, - outbox_result: receipt.outbox_result, - } +fn sdk_backup_manifest_preview( + output: &Path, + status: &StorageStatus, + integrity: &IntegrityStatus, +) -> Value { + json!({ + "manifest_kind": "sdk_canonical_backup_preview", + "destination": output.display().to_string(), + "backup_paths": { + "runtime_path": output.join(SDK_RUNTIME_FILE).display().to_string(), + "private_path": output.join(SDK_PRIVATE_FILE).display().to_string(), + }, + "source_status": status, + "integrity": integrity, + }) } fn ensure_safe_sdk_backup_destination( @@ -469,13 +513,11 @@ fn ensure_safe_sdk_backup_destination( let sdk_root = sdk_storage_root(config); let sdk_runtime_path = sdk_root.join(SDK_RUNTIME_FILE); let sdk_private_path = sdk_root.join(SDK_PRIVATE_FILE); - let sdk_studio_path = sdk_root.join(SDK_STUDIO_FILE); let forbidden_paths = [ sdk_root.as_path(), config.local.replica_store_path.as_path(), sdk_runtime_path.as_path(), sdk_private_path.as_path(), - sdk_studio_path.as_path(), ]; if forbidden_paths.contains(&output) { return Err(RuntimeError::Config(format!( @@ -499,13 +541,11 @@ fn ensure_safe_sdk_restore_destination( let sdk_root = sdk_storage_root(config); let sdk_runtime_path = sdk_root.join(SDK_RUNTIME_FILE); let sdk_private_path = sdk_root.join(SDK_PRIVATE_FILE); - let sdk_studio_path = sdk_root.join(SDK_STUDIO_FILE); let forbidden_paths = [ config.local.root.as_path(), config.local.replica_store_path.as_path(), sdk_runtime_path.as_path(), sdk_private_path.as_path(), - sdk_studio_path.as_path(), ]; if forbidden_paths.contains(&destination) { return Err(RuntimeError::Config(format!( @@ -525,148 +565,42 @@ fn ensure_safe_sdk_restore_destination( Ok(()) } -fn sdk_backup_view(receipt: BackupReceipt) -> Result<LocalBackupView, CliSdkAdapterError> { - let event_store_file = receipt - .runtime_path - .as_ref() - .map(|path| display_path(path.as_path())); - let outbox_file = receipt - .runtime_path - .as_ref() - .map(|path| display_path(path.as_path())); - let manifest_file = receipt - .manifest_path - .as_ref() - .map(|path| display_path(path.as_path())); - let size_bytes = path_size(receipt.runtime_path.as_ref())? - + path_size(receipt.private_path.as_ref())? - + path_size(receipt.studio_path.as_ref())? - + path_size(receipt.manifest_path.as_ref())?; - Ok(LocalBackupView { - state: sdk_backup_state_label(receipt.state).to_owned(), - source: SDK_CANONICAL_SOURCE.to_owned(), - backup_kind: SDK_BACKUP_KIND.to_owned(), - canonical_store: SDK_CANONICAL_STORE.to_owned(), - destination: display_path(&receipt.destination), - file: manifest_file - .clone() - .unwrap_or_else(|| receipt.destination.display().to_string()), - event_store_file, - outbox_file, - manifest_file, - size_bytes, - manifest: json_value(&receipt.manifest)?, - reason: None, - actions: Vec::new(), - }) -} - -fn sdk_restore_view( - receipt: RestoreReceipt, - overwrite: bool, - dry_run: bool, -) -> Result<LocalRestoreView, CliSdkAdapterError> { - let destination_paths = receipt.destination_paths.as_ref(); - let restored_paths = receipt.restored_paths.as_ref(); - Ok(LocalRestoreView { - state: sdk_restore_state_label(receipt.state).to_owned(), - source: SDK_CANONICAL_SOURCE.to_owned(), - restore_kind: SDK_BACKUP_KIND.to_owned(), - canonical_store: SDK_CANONICAL_STORE.to_owned(), - backup_source: display_path(&receipt.source), - destination: receipt - .destination - .as_ref() - .map(|path| display_path(path.as_path())) - .unwrap_or_default(), - event_store_file: display_path(&receipt.runtime_path), - outbox_file: display_path(&receipt.runtime_path), - manifest_file: display_path(&receipt.manifest_path), - destination_event_store_file: destination_paths - .map(|paths| display_path(&paths.runtime_path)), - destination_outbox_file: destination_paths.map(|paths| display_path(&paths.runtime_path)), - restored_event_store_file: restored_paths.map(|paths| display_path(&paths.runtime_path)), - restored_outbox_file: restored_paths.map(|paths| display_path(&paths.runtime_path)), - manifest: json_value(&receipt.manifest)?, - verification: json_value(&receipt.verification)?, - overwrite, - dry_run, - reason: if dry_run { - Some("dry run requested; SDK canonical store was not restored".to_owned()) - } else { - None - }, - actions: if dry_run { - vec!["radroots store restore <backup-dir>".to_owned()] - } else { - Vec::new() - }, - }) -} - -fn sdk_restore_state_label(state: SdkRestoreState) -> &'static str { - match state { - SdkRestoreState::Validated => "validated", - SdkRestoreState::DryRun => "dry_run", - SdkRestoreState::Completed => "completed", - _ => "unknown", - } -} - -fn sdk_backup_manifest_preview( - output: &Path, - status: &StorageStatusReceipt, - integrity: &IntegrityReceipt, -) -> Value { - json!({ - "manifest_kind": "sdk_canonical_backup_preview", - "destination": output.display().to_string(), - "source_storage": sdk_storage_kind_label(status.storage), - "source_paths": &status.paths, - "backup_paths": { - "runtime_path": output.join(SDK_RUNTIME_FILE).display().to_string(), - "private_path": output.join(SDK_PRIVATE_FILE).display().to_string(), - "studio_path": output.join(SDK_STUDIO_FILE).display().to_string(), - }, - "source_status": status, - "backup_verification": { - "event_store_ok": integrity.event_store_ok, - "outbox_ok": integrity.outbox_ok, - "event_store_result": &integrity.event_store_result, - "outbox_result": &integrity.outbox_result, - }, - }) -} - -fn sdk_storage_kind_label(kind: SdkStorageKind) -> &'static str { - match kind { - SdkStorageKind::Memory => "memory", - SdkStorageKind::Directory => "directory", - _ => "unknown", - } -} - -fn sdk_backup_state_label(state: SdkBackupState) -> &'static str { - match state { - SdkBackupState::Planned => "planned", - SdkBackupState::Completed => "completed", - _ => "unknown", - } -} - fn json_value(value: impl Serialize) -> Result<Value, RuntimeError> { serde_json::to_value(value).map_err(RuntimeError::from) } -fn path_size(path: Option<&PathBuf>) -> Result<u64, RuntimeError> { - path.map(fs::metadata) - .transpose()? - .map(|metadata| metadata.len()) - .ok_or_else(|| RuntimeError::Config("SDK backup did not report all file paths".to_owned())) -} - -fn display_path(path: &Path) -> String { - path.display().to_string() +fn sdk_backup_plan() -> Result<BackupPlan, RuntimeError> { + let mut bytes = [0_u8; 16]; + getrandom::getrandom(&mut bytes) + .map_err(|error| RuntimeError::Config(format!("failed to generate backup ID: {error}")))?; + let backup_id = BackupId::new(bytes) + .map_err(|error| RuntimeError::Config(format!("invalid backup ID: {error}")))?; + BackupPlan::new( + backup_id, + BackupFormatVersion::V1, + BackupSecretPolicy::ExcludeProtectedStorage, + unix_ms()?, + ) + .map_err(|error| RuntimeError::Config(format!("invalid backup plan: {error}"))) +} + +fn unix_ms() -> Result<u64, RuntimeError> { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map_err(|error| RuntimeError::Config(format!("system clock error: {error}")))? + .as_millis() + .try_into() + .map_err(|_| RuntimeError::Config("system clock is outside SDK range".to_owned())) +} + +fn hex_bytes(bytes: &[u8]) -> String { + const HEX: &[u8; 16] = b"0123456789abcdef"; + let mut encoded = String::with_capacity(bytes.len() * 2); + for byte in bytes { + encoded.push(char::from(HEX[usize::from(byte >> 4)])); + encoded.push(char::from(HEX[usize::from(byte & 0x0f)])); + } + encoded } fn create_parent_dir(path: &Path) -> Result<(), RuntimeError> { diff --git a/src/runtime/sync.rs b/src/runtime/sync.rs @@ -1,7 +1,7 @@ use std::thread; use std::time::{Duration, SystemTime, UNIX_EPOCH}; -use radroots_event::kinds::{ +use radroots_event::envelope::kind::{ KIND_CLASSIFIED_LISTING, KIND_FARM, KIND_LIST_SET_APP_CURATION, KIND_LIST_SET_BOOKMARK, KIND_LIST_SET_CALENDAR, KIND_LIST_SET_CURATION, KIND_LIST_SET_EMOJI, KIND_LIST_SET_FOLLOW, KIND_LIST_SET_GENERIC, KIND_LIST_SET_INTEREST, KIND_LIST_SET_KIND_MUTE, @@ -1780,16 +1780,16 @@ fn relative_age(age_seconds: u64) -> String { mod tests { use std::path::{Path, PathBuf}; - use radroots_event::farm::{RadrootsFarm, RadrootsFarmRef}; - use radroots_event::ids::RadrootsEventId; - use radroots_event::kinds::{ + use radroots_event::envelope::kind::{ KIND_CLASSIFIED_LISTING, KIND_FARM, KIND_LIST_SET_GENERIC, KIND_POST, }; + use radroots_event::farm::{Farm, FarmRef}; + use radroots_event::id::RadrootsEventId; use radroots_event::list::RadrootsListEntry; use radroots_event::list_set::RadrootsListSet; use radroots_event::plot::RadrootsPlot; - use radroots_event::profile::RadrootsAuthoredProfile; - use radroots_event::wire::{DEFAULT_CONTENT_MAX_BYTES, RadrootsNip01EventWireParts}; + use radroots_event::profile::AuthoredProfile; + use radroots_event::wire::{DEFAULT_CONTENT_MAX_BYTES, Nip01EventWireParts}; use radroots_event_codec::farm::encode as farm_encode; use radroots_event_codec::list_set::encode as list_set_encode; use radroots_event_codec::plot::encode as plot_encode; @@ -2954,7 +2954,7 @@ mod tests { let events = vec![ signed_event( &seller, - RadrootsNip01EventWireParts { + Nip01EventWireParts { kind: KIND_CLASSIFIED_LISTING, content: "x".repeat(DEFAULT_CONTENT_MAX_BYTES + 1), tags: Vec::new(), @@ -2962,7 +2962,7 @@ mod tests { ), signed_event( &seller, - RadrootsNip01EventWireParts { + Nip01EventWireParts { kind: KIND_POST, content: "hello".to_owned(), tags: Vec::new(), @@ -2970,7 +2970,7 @@ mod tests { ), signed_event( &seller, - RadrootsNip01EventWireParts { + Nip01EventWireParts { kind: KIND_CLASSIFIED_LISTING, content: "not a listing".to_owned(), tags: Vec::new(), @@ -3052,7 +3052,7 @@ mod tests { } fn profile_event(identity: &RadrootsIdentity) -> RadrootsNostrEvent { - let profile = RadrootsAuthoredProfile::new("seller") + let profile = AuthoredProfile::new("seller") .expect("profile") .with_display_name("Seller") .with_about("market seller"); @@ -3063,7 +3063,7 @@ mod tests { } fn farm_event(identity: &RadrootsIdentity) -> RadrootsNostrEvent { - let farm = RadrootsFarm { + let farm = Farm { d_tag: FARM_D_TAG.to_owned(), name: "Relay Farm".to_owned(), about: Some("relay farm".to_owned()), @@ -3082,7 +3082,7 @@ mod tests { fn plot_event(identity: &RadrootsIdentity) -> RadrootsNostrEvent { let plot = RadrootsPlot { d_tag: PLOT_D_TAG.to_owned(), - farm: RadrootsFarmRef { + farm: FarmRef { pubkey: identity.public_key_hex(), d_tag: FARM_D_TAG.to_owned(), }, @@ -3125,7 +3125,7 @@ mod tests { title: &str, created_at: u64, ) -> RadrootsNostrEvent { - let mut builder = wire_fixture_builder(RadrootsNip01EventWireParts { + let mut builder = wire_fixture_builder(Nip01EventWireParts { kind: KIND_CLASSIFIED_LISTING, content: "# Pasture Eggs".to_owned(), tags: vec![ @@ -3175,16 +3175,13 @@ mod tests { .expect("signed event") } - fn signed_event( - identity: &RadrootsIdentity, - parts: RadrootsNip01EventWireParts, - ) -> RadrootsNostrEvent { + fn signed_event(identity: &RadrootsIdentity, parts: Nip01EventWireParts) -> RadrootsNostrEvent { wire_fixture_builder(parts) .sign_with_keys(identity.keys()) .expect("signed event") } - fn wire_fixture_builder(parts: RadrootsNip01EventWireParts) -> nostr::EventBuilder { + fn wire_fixture_builder(parts: Nip01EventWireParts) -> nostr::EventBuilder { let kind = u16::try_from(parts.kind).expect("fixture kind must fit NIP-01"); let tags = parts .tags diff --git a/src/runtime/trade.rs b/src/runtime/trade.rs @@ -3,42 +3,33 @@ use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicU64, Ordering}; use std::time::{SystemTime, UNIX_EPOCH}; -use radroots_authority::RadrootsActorContext; -use radroots_core::{ - RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreUnit, convert_unit_decimal, -}; -use radroots_event::contract::RadrootsActorRole; -use radroots_event::ids::{ - RadrootsClassifiedListingAddress, RadrootsDTag, RadrootsInventoryBinId, RadrootsPublicKey, - RadrootsTradeCandidateId, RadrootsTradeId, RadrootsTradeMutationId, +use radroots_core::unit::convert_unit_decimal; +use radroots_core::{Currency, Decimal, Unit}; +use radroots_event::contract::AuthorRole; +use radroots_event::id::{ + CandidateId, ClassifiedListingAddress, DTag, InventoryBinId, MutationId, TradeId, }; use radroots_event::trade::{ - RADROOTS_TRADE_PROPOSAL_CONTRACT_ID, RADROOTS_TRADE_SCHEMA_VERSION, - RadrootsFulfillmentProfileV1, RadrootsTradeCancellationProfileV1, RadrootsTradeCandidateLineV1, - RadrootsTradeCandidateTermsV1, RadrootsTradeEconomicsProfileV1, RadrootsTradeMutationBodyV1, - RadrootsTradeMutationEnvelopeV1, canonical_trade_mutation_content, + FulfillmentProfileV1, RADROOTS_TRADE_PROPOSAL_CONTRACT_ID, RADROOTS_TRADE_SCHEMA_VERSION, + TradeCancellationProfileV1, TradeCandidateLineV1, TradeCandidateTermsV1, + TradeEconomicsProfileV1, TradeMutationBodyV1, TradeMutationEnvelopeV1, + canonical_trade_mutation_content, }; +use radroots_identity::PublicKey; use radroots_replica_schema::nostr_event_head::{ INostrEventHeadFindOne, INostrEventHeadFindOneArgs, NostrEventHeadQueryBindValues, }; use radroots_replica_schema::trade_product::{ITradeProductFieldsFilter, ITradeProductFindMany}; use radroots_replica_store::{ReplicaSql, nostr_event_head, trade_product}; -use radroots_sdk::{ - CancelTradeRequest, DecideCandidateRequest, EvidenceRefreshReceipt, EvidenceView, - GetTradeRequest, InspectEvidenceRequest, ListTradesRequest, Page, ProposeRevisionRequest, - RefreshTradeEvidenceRequest, ResumeOperationRequest, SdkIdempotencyKey, SubmitProposalRequest, - TradeCommandReceipt, TradePrivateArtifactDeleteReceipt, TradePrivateArtifactDeleteRequest, - TradePrivateArtifactKind, TradePrivateArtifactOpenReceipt, TradePrivateArtifactOpenRequest, - TradePrivateArtifactSealReceipt, TradePrivateArtifactSealRequest, TradeStatusView, - TradeSummaryView, -}; +use radroots_sdk::trade::{self as sdk_trade, Plan as TradePlan}; +use radroots_signing::{Actor, actor::ActorSource}; use radroots_sql_core::SqlxSqliteExecutor; use serde::Serialize; use crate::runtime::RuntimeError; use crate::runtime::account; use crate::runtime::config::RuntimeConfig; -use crate::runtime::sdk::{CliSdkAdapterError, CliSdkSession, sdk_target_policy}; +use crate::runtime::sdk::{CliSdkAdapterError, validate_configured_signer_for_actor}; const TRADE_CANDIDATE_DRAFTS_DIR: &str = "trades/candidates"; const TRADE_CANDIDATE_DRAFT_SOURCE: &str = "SDK trade proposal candidate draft"; @@ -148,7 +139,7 @@ pub struct TradePrivateArtifactOpenView { pub artifact_id: String, pub trade_id: Option<String>, pub candidate_id: Option<String>, - pub artifact_kind: Option<TradePrivateArtifactKind>, + pub artifact_kind: Option<String>, pub schema_id: Option<String>, pub retention_class: Option<String>, pub output: String, @@ -158,194 +149,207 @@ pub struct TradePrivateArtifactOpenView { pub deleted_at_ms: Option<i64>, } +#[derive(Debug, Clone, Serialize)] +pub struct TradePreparedView { + pub state: String, + pub operation: String, + pub trade_id: String, + pub mutation_id: String, + pub mutation_kind: String, + pub event_id: String, + pub event_kind: u32, + pub author: String, + pub required_actions: Vec<String>, + pub idempotency_key: Option<String>, + pub reason: Option<String>, + pub actions: Vec<String>, +} + pub fn submit_proposal( config: &RuntimeConfig, args: &TradeEnvelopeFileRuntimeArgs, -) -> Result<TradeCommandReceipt, CliSdkAdapterError> { - let envelope = load_trade_envelope(args.file.as_path())?; - let (actor, session) = actor_session_for_envelope(config, &envelope, "trade proposal")?; - let mut request = SubmitProposalRequest::new(actor, envelope, sdk_target_policy(config)); - if let Some(idempotency_key) = idempotency_key(args.idempotency_key.as_deref())? { - request = request.with_idempotency_key(idempotency_key); - } - Ok(session.block_on(session.sdk().trades().commands().submit_proposal(request))?) +) -> Result<TradePreparedView, CliSdkAdapterError> { + prepare_trade_command(config, args, "trade.submit_proposal.v1") } pub fn propose_revision( config: &RuntimeConfig, args: &TradeEnvelopeFileRuntimeArgs, -) -> Result<TradeCommandReceipt, CliSdkAdapterError> { - let envelope = load_trade_envelope(args.file.as_path())?; - let (actor, session) = actor_session_for_envelope(config, &envelope, "trade revision")?; - let mut request = ProposeRevisionRequest::new(actor, envelope, sdk_target_policy(config)); - if let Some(idempotency_key) = idempotency_key(args.idempotency_key.as_deref())? { - request = request.with_idempotency_key(idempotency_key); - } - Ok(session.block_on(session.sdk().trades().commands().propose_revision(request))?) +) -> Result<TradePreparedView, CliSdkAdapterError> { + prepare_trade_command(config, args, "trade.propose_revision.v1") } pub fn decide_candidate( config: &RuntimeConfig, args: &TradeEnvelopeFileRuntimeArgs, -) -> Result<TradeCommandReceipt, CliSdkAdapterError> { - let envelope = load_trade_envelope(args.file.as_path())?; - let (actor, session) = - actor_session_for_envelope(config, &envelope, "trade candidate decision")?; - let mut request = DecideCandidateRequest::new(actor, envelope, sdk_target_policy(config)); - if args.acknowledge_private_terms { - request = request.acknowledge_private_terms(); - } - if let Some(idempotency_key) = idempotency_key(args.idempotency_key.as_deref())? { - request = request.with_idempotency_key(idempotency_key); - } - Ok(session.block_on(session.sdk().trades().commands().decide_candidate(request))?) +) -> Result<TradePreparedView, CliSdkAdapterError> { + prepare_trade_command(config, args, "trade.decide_candidate.v1") } pub fn cancel_trade( config: &RuntimeConfig, args: &TradeEnvelopeFileRuntimeArgs, -) -> Result<TradeCommandReceipt, CliSdkAdapterError> { - let envelope = load_trade_envelope(args.file.as_path())?; - let (actor, session) = actor_session_for_envelope(config, &envelope, "trade cancellation")?; - let mut request = CancelTradeRequest::new(actor, envelope, sdk_target_policy(config)); - if let Some(idempotency_key) = idempotency_key(args.idempotency_key.as_deref())? { - request = request.with_idempotency_key(idempotency_key); - } - Ok(session.block_on(session.sdk().trades().commands().cancel_trade(request))?) +) -> Result<TradePreparedView, CliSdkAdapterError> { + prepare_trade_command(config, args, "trade.cancel.v1") } pub fn resume_operation( config: &RuntimeConfig, args: &TradeEnvelopeFileRuntimeArgs, -) -> Result<TradeCommandReceipt, CliSdkAdapterError> { +) -> Result<TradePreparedView, CliSdkAdapterError> { let operation_kind = args.operation_kind.as_deref().ok_or_else(|| { RuntimeError::Config("trade operation resume requires `--operation-kind`".to_owned()) })?; + prepare_trade_command(config, args, resume_operation_kind(operation_kind)?) +} + +fn prepare_trade_command( + config: &RuntimeConfig, + args: &TradeEnvelopeFileRuntimeArgs, + operation: &str, +) -> Result<TradePreparedView, CliSdkAdapterError> { let envelope = load_trade_envelope(args.file.as_path())?; - let (actor, session) = actor_session_for_envelope(config, &envelope, "trade operation resume")?; - let mut request = ResumeOperationRequest::new( - actor, - envelope, - resume_operation_kind(operation_kind)?, - sdk_target_policy(config), - ); - if args.acknowledge_private_terms { - request = request.acknowledge_private_terms(); + let actor = actor_for_envelope(config, &envelope, operation)?; + let idempotency_key = args + .idempotency_key + .as_deref() + .and_then(non_empty_ref) + .map(radroots_storage::journal::IdempotencyKey::parse) + .transpose() + .map_err(|error| RuntimeError::Config(format!("invalid idempotency key: {error}")))?; + let plan = sdk_trade::prepare(sdk_trade::PrepareRequest::new(actor, envelope)) + .map_err(|error| RuntimeError::Config(format!("invalid SDK trade plan: {error}")))?; + if matches!(operation, "trade.decide_candidate.v1") + && plan.workflow().private_terms().is_some() + && !args.acknowledge_private_terms + { + return Err(RuntimeError::Config( + "trade decision requires explicit private-terms acknowledgement".to_owned(), + ) + .into()); } - if let Some(idempotency_key) = idempotency_key(args.idempotency_key.as_deref())? { - request = request.with_idempotency_key(idempotency_key); + Ok(trade_prepared_view(operation, args, &plan, idempotency_key)) +} + +fn trade_prepared_view( + operation: &str, + args: &TradeEnvelopeFileRuntimeArgs, + plan: &TradePlan, + idempotency_key: Option<radroots_storage::journal::IdempotencyKey>, +) -> TradePreparedView { + TradePreparedView { + state: "prepared".to_owned(), + operation: operation.to_owned(), + trade_id: plan.workflow().trade_id().as_str().to_owned(), + mutation_id: plan.workflow().mutation_id().as_str().to_owned(), + mutation_kind: format!("{:?}", plan.workflow().kind()), + event_id: plan.draft().expected_event_id().as_str().to_owned(), + event_kind: plan.draft().kind_u32(), + author: plan.draft().expected_pubkey().to_hex(), + required_actions: plan + .workflow() + .required_actions() + .iter() + .map(|action| format!("{action:?}")) + .collect(), + idempotency_key: idempotency_key.map(|value| value.as_str().to_owned()), + reason: Some( + "trade plan validated; durable enqueue requires the configured shared sync engine" + .to_owned(), + ), + actions: vec![format!( + "radroots trade operation resume {}", + args.file.display() + )], } - Ok(session.block_on(session.sdk().trades().commands().resume_operation(request))?) } pub fn get_trade( - config: &RuntimeConfig, + _config: &RuntimeConfig, args: &TradeIdRuntimeArgs, -) -> Result<TradeStatusView, CliSdkAdapterError> { - let session = CliSdkSession::connect(config)?; - let trade_id = trade_id(args.trade_id.as_str(), "trade_id")?; - let request = GetTradeRequest::new(trade_id); - Ok(session.block_on(session.sdk().trades().queries().get_trade(request))?) +) -> Result<serde_json::Value, CliSdkAdapterError> { + let _ = trade_id(args.trade_id.as_str(), "trade_id")?; + Err(RuntimeError::Config( + "trade queries require the configured SDK storage and projection adapter".to_owned(), + ) + .into()) } pub fn list_trades( - config: &RuntimeConfig, + _config: &RuntimeConfig, args: &TradePageRuntimeArgs, -) -> Result<Page<TradeSummaryView>, CliSdkAdapterError> { - let session = CliSdkSession::connect(config)?; - let mut request = ListTradesRequest::new(); - if let Some(limit) = args.limit { - request = request.with_limit(limit); - } - if let Some(cursor) = args.cursor.as_deref().and_then(non_empty_ref) { - request = request.with_cursor(cursor); +) -> Result<serde_json::Value, CliSdkAdapterError> { + if matches!(args.limit, Some(0)) { + return Err(RuntimeError::Config("trade list limit must be positive".to_owned()).into()); } - Ok(session.block_on(session.sdk().trades().queries().list_trades(request))?) + let _ = args.cursor.as_deref().and_then(non_empty_ref); + Err(RuntimeError::Config( + "trade queries require the configured SDK storage and projection adapter".to_owned(), + ) + .into()) } pub fn refresh_evidence( - config: &RuntimeConfig, + _config: &RuntimeConfig, args: &TradeIdRuntimeArgs, -) -> Result<EvidenceRefreshReceipt, CliSdkAdapterError> { - let session = CliSdkSession::connect(config)?; - let trade_id = trade_id(args.trade_id.as_str(), "trade_id")?; - let request = RefreshTradeEvidenceRequest::new(trade_id); - Ok(session.block_on(session.sdk().trades().queries().refresh_evidence(request))?) +) -> Result<serde_json::Value, CliSdkAdapterError> { + let _ = trade_id(args.trade_id.as_str(), "trade_id")?; + Err(RuntimeError::Config( + "trade projection refresh requires the configured shared sync engine".to_owned(), + ) + .into()) } pub fn inspect_evidence( - config: &RuntimeConfig, + _config: &RuntimeConfig, args: &TradeEvidenceInspectRuntimeArgs, -) -> Result<Page<EvidenceView>, CliSdkAdapterError> { - let session = CliSdkSession::connect(config)?; - let mut request = InspectEvidenceRequest::new(trade_id(args.trade_id.as_str(), "trade_id")?); - if let Some(limit) = args.limit { - request = request.with_limit(limit); - } - if let Some(cursor) = args.cursor.as_deref().and_then(non_empty_ref) { - request = request.with_cursor(cursor); +) -> Result<serde_json::Value, CliSdkAdapterError> { + let _ = trade_id(args.trade_id.as_str(), "trade_id")?; + if matches!(args.limit, Some(0)) { + return Err( + RuntimeError::Config("trade evidence limit must be positive".to_owned()).into(), + ); } - Ok(session.block_on(session.sdk().trades().queries().inspect_evidence(request))?) + let _ = args.cursor.as_deref().and_then(non_empty_ref); + Err(RuntimeError::Config( + "trade evidence queries require the configured SDK storage adapter".to_owned(), + ) + .into()) } pub fn seal_private_artifact( - config: &RuntimeConfig, + _config: &RuntimeConfig, args: &TradePrivateArtifactSealRuntimeArgs, -) -> Result<TradePrivateArtifactSealReceipt, CliSdkAdapterError> { - let session = CliSdkSession::connect(config)?; - let plaintext = fs::read(args.input.as_path()).map_err(RuntimeError::from)?; - let mut request = TradePrivateArtifactSealRequest::binding_terms( - args.artifact_id.as_str(), - trade_id(args.trade_id.as_str(), "trade_id")?, - args.schema_id.as_str(), - plaintext, - ); - request.artifact_kind = private_artifact_kind(args.kind.as_str())?; - if let Some(candidate_id) = args.candidate_id.as_deref().and_then(non_empty_ref) { - request = request.with_candidate_id(trade_candidate_id(candidate_id, "candidate_id")?); - } - if let Some(retention_class) = args.retention_class.as_deref().and_then(non_empty_ref) { - request = request.with_retention_class(retention_class); - } - if let Some(expires_at_ms) = args.expires_at_ms { - request = request.with_expires_at_ms(expires_at_ms); - } - Ok(session.block_on(session.sdk().trades().seal_private_artifact(request))?) +) -> Result<serde_json::Value, CliSdkAdapterError> { + let _ = trade_id(args.trade_id.as_str(), "trade_id")?; + let _ = private_artifact_kind(args.kind.as_str())?; + let _ = fs::metadata(args.input.as_path()).map_err(RuntimeError::from)?; + Err(RuntimeError::Config( + "private trade artifact sealing requires a host-owned secret adapter".to_owned(), + ) + .into()) } pub fn open_private_artifact( - config: &RuntimeConfig, + _config: &RuntimeConfig, args: &TradePrivateArtifactOpenRuntimeArgs, ) -> Result<TradePrivateArtifactOpenView, CliSdkAdapterError> { - let session = CliSdkSession::connect(config)?; - let request = TradePrivateArtifactOpenRequest::new(args.artifact_id.as_str()); - let receipt = session.block_on(session.sdk().trades().open_private_artifact(request))?; - Ok(match receipt { - Some(receipt) => private_artifact_open_view(args, receipt)?, - None => TradePrivateArtifactOpenView { - state: "missing".to_owned(), - artifact_id: args.artifact_id.clone(), - trade_id: None, - candidate_id: None, - artifact_kind: None, - schema_id: None, - retention_class: None, - output: args.output.display().to_string(), - bytes_written: 0, - created_at_ms: None, - expires_at_ms: None, - deleted_at_ms: None, - }, - }) + Err(RuntimeError::Config(format!( + "private trade artifact `{}` requires a host-owned secret adapter", + args.artifact_id + )) + .into()) } pub fn delete_private_artifact( - config: &RuntimeConfig, + _config: &RuntimeConfig, args: &TradePrivateArtifactDeleteRuntimeArgs, -) -> Result<TradePrivateArtifactDeleteReceipt, CliSdkAdapterError> { - let session = CliSdkSession::connect(config)?; - let request = TradePrivateArtifactDeleteRequest::new(args.artifact_id.as_str()); - Ok(session.block_on(session.sdk().trades().delete_private_artifact(request))?) +) -> Result<serde_json::Value, CliSdkAdapterError> { + Err(RuntimeError::Config(format!( + "private trade artifact `{}` requires a host-owned secret adapter", + args.artifact_id + )) + .into()) } pub fn scaffold_proposal_draft( @@ -393,7 +397,7 @@ fn scaffold_proposal_draft_inner( farm_id.clone(), args.quantity, )?; - let envelope = RadrootsTradeMutationEnvelopeV1 { + let envelope = TradeMutationEnvelopeV1 { mutation_id: None, contract_id: RADROOTS_TRADE_PROPOSAL_CONTRACT_ID.to_owned(), schema_version: RADROOTS_TRADE_SCHEMA_VERSION, @@ -409,7 +413,7 @@ fn scaffold_proposal_draft_inner( )?, counterparty_pubkey: seller_pubkey, authored_at_unix_s: now_unix(), - body: RadrootsTradeMutationBodyV1::Proposal { candidate }, + body: TradeMutationBodyV1::Proposal { candidate }, }; let canonical = canonical_trade_mutation_content(envelope) .map_err(|error| RuntimeError::Config(format!("build trade proposal envelope: {error}")))?; @@ -451,7 +455,7 @@ fn scaffold_proposal_draft_inner( }) } -fn load_trade_envelope(path: &Path) -> Result<RadrootsTradeMutationEnvelopeV1, RuntimeError> { +fn load_trade_envelope(path: &Path) -> Result<TradeMutationEnvelopeV1, RuntimeError> { let contents = fs::read_to_string(path)?; serde_json::from_str(contents.as_str()).map_err(|error| { RuntimeError::Config(format!( @@ -461,11 +465,11 @@ fn load_trade_envelope(path: &Path) -> Result<RadrootsTradeMutationEnvelopeV1, R }) } -fn actor_session_for_envelope( +fn actor_for_envelope( config: &RuntimeConfig, - envelope: &RadrootsTradeMutationEnvelopeV1, + envelope: &TradeMutationEnvelopeV1, operation: &str, -) -> Result<(RadrootsActorContext, CliSdkSession), CliSdkAdapterError> { +) -> Result<Actor, CliSdkAdapterError> { let account = account::resolve_account(config)?.ok_or_else(|| { RuntimeError::Config(format!("{operation} requires a selected signer account")) })?; @@ -483,40 +487,29 @@ fn actor_session_for_envelope( .as_str() .eq_ignore_ascii_case(author_pubkey) { - RadrootsActorRole::Buyer + AuthorRole::Buyer } else if envelope .seller_pubkey .as_str() .eq_ignore_ascii_case(author_pubkey) { - RadrootsActorRole::Seller + AuthorRole::Seller } else { return Err(RuntimeError::Config(format!( "{operation} envelope author must be the buyer or seller" )) .into()); }; - let actor = RadrootsActorContext::local_account( - author_pubkey, - account.record.account_id.to_string(), - [role], - ) - .map_err(|error| RuntimeError::Config(format!("invalid trade SDK actor: {error}")))?; - let session = CliSdkSession::connect_for_actor( + let actor = + Actor::from_public_key_hex(author_pubkey, ActorSource::ExplicitPublicKey, [role]) + .map_err(|error| RuntimeError::Config(format!("invalid trade SDK actor: {error}")))?; + validate_configured_signer_for_actor( config, Some(account.record.account_id.as_str()), author_pubkey, operation, )?; - Ok((actor, session)) -} - -fn idempotency_key(value: Option<&str>) -> Result<Option<SdkIdempotencyKey>, RuntimeError> { - value - .and_then(non_empty_ref) - .map(SdkIdempotencyKey::new) - .transpose() - .map_err(|error| RuntimeError::Config(error.to_string())) + Ok(actor) } fn resume_operation_kind(value: &str) -> Result<&'static str, RuntimeError> { @@ -531,36 +524,12 @@ fn resume_operation_kind(value: &str) -> Result<&'static str, RuntimeError> { } } -fn private_artifact_open_view( - args: &TradePrivateArtifactOpenRuntimeArgs, - receipt: TradePrivateArtifactOpenReceipt, -) -> Result<TradePrivateArtifactOpenView, RuntimeError> { - if let Some(parent) = args.output.parent() { - fs::create_dir_all(parent)?; - } - fs::write(args.output.as_path(), &receipt.plaintext)?; - Ok(TradePrivateArtifactOpenView { - state: "opened".to_owned(), - artifact_id: receipt.artifact_id, - trade_id: Some(receipt.trade_id.to_string()), - candidate_id: receipt.candidate_id.as_ref().map(ToString::to_string), - artifact_kind: Some(receipt.artifact_kind), - schema_id: Some(receipt.schema_id), - retention_class: Some(receipt.retention_class), - output: args.output.display().to_string(), - bytes_written: receipt.plaintext.len(), - created_at_ms: Some(receipt.created_at_ms), - expires_at_ms: receipt.expires_at_ms, - deleted_at_ms: receipt.deleted_at_ms, - }) -} - -fn private_artifact_kind(value: &str) -> Result<TradePrivateArtifactKind, RuntimeError> { +fn private_artifact_kind(value: &str) -> Result<&'static str, RuntimeError> { match value { - "binding_terms" => Ok(TradePrivateArtifactKind::BindingTerms), - "message" => Ok(TradePrivateArtifactKind::Message), - "contact_bundle" => Ok(TradePrivateArtifactKind::ContactBundle), - "delivery_instruction" => Ok(TradePrivateArtifactKind::DeliveryInstruction), + "binding_terms" => Ok("binding_terms"), + "message" => Ok("message"), + "contact_bundle" => Ok("contact_bundle"), + "delivery_instruction" => Ok("delivery_instruction"), other => Err(RuntimeError::Config(format!( "unsupported private artifact kind `{other}`" ))), @@ -687,50 +656,45 @@ fn resolve_product( fn candidate_terms( product: &ProductFacts, listing_state: &ActiveListingState, - buyer_pubkey: RadrootsPublicKey, - seller_pubkey: RadrootsPublicKey, - farm_id: RadrootsDTag, + buyer_pubkey: PublicKey, + seller_pubkey: PublicKey, + farm_id: DTag, bin_count: u32, -) -> Result<RadrootsTradeCandidateTermsV1, RuntimeError> { +) -> Result<TradeCandidateTermsV1, RuntimeError> { let currency = product .price_currency - .parse::<RadrootsCoreCurrency>() + .parse::<Currency>() .map_err(|error| { RuntimeError::Config(format!("listing price_currency is invalid: {error}")) })?; let quantity_amount = exact_positive_decimal(product.qty_amt_exact.as_str(), "qty_amt_exact")? - * RadrootsCoreDecimal::from(bin_count); + * Decimal::from(bin_count); let quantity_unit = product .qty_unit - .parse::<RadrootsCoreUnit>() + .parse::<Unit>() .map_err(|error| RuntimeError::Config(format!("listing qty_unit is invalid: {error}")))?; let price_amount = exact_positive_decimal(product.price_amt_exact.as_str(), "price_amt_exact")?; let price_quantity_amount = exact_positive_decimal(product.price_qty_amt_exact.as_str(), "price_qty_amt_exact")?; - let price_unit = product - .price_qty_unit - .parse::<RadrootsCoreUnit>() - .map_err(|error| { - RuntimeError::Config(format!("listing price_qty_unit is invalid: {error}")) - })?; + let price_unit = product.price_qty_unit.parse::<Unit>().map_err(|error| { + RuntimeError::Config(format!("listing price_qty_unit is invalid: {error}")) + })?; let quantity_unit_in_price_units = - convert_unit_decimal(RadrootsCoreDecimal::ONE, quantity_unit, price_unit).map_err( - |error| { - RuntimeError::Config(format!( - "listing quantity and price units are incompatible: {error}" - )) - }, - )?; + convert_unit_decimal(Decimal::ONE, quantity_unit, price_unit).map_err(|error| { + RuntimeError::Config(format!( + "listing quantity and price units are incompatible: {error}" + )) + })?; let unit_price_amount = (price_amount / price_quantity_amount) * quantity_unit_in_price_units; let subtotal = unit_price_amount * quantity_amount; let quantity_scale = u8::try_from(quantity_amount.scale()) .map_err(|_| RuntimeError::Config("trade quantity scale exceeds u8".to_owned()))?; let currency_exponent = currency.minor_unit_exponent(); let subtotal_mantissa = decimal_mantissa_at_scale(subtotal, currency_exponent); - let line = RadrootsTradeCandidateLineV1 { - line_id: RadrootsDTag::parse("line-1") + let line = TradeCandidateLineV1 { + line_id: DTag::parse("line-1") .map_err(|error| RuntimeError::Config(format!("invalid line id: {error}")))?, - listing_addr: RadrootsClassifiedListingAddress::parse(product.listing_addr.as_str()) + listing_addr: ClassifiedListingAddress::parse(product.listing_addr.as_str()) .map_err(|error| RuntimeError::Config(format!("invalid listing address: {error}")))?, listing_event_id: listing_state .last_event_id @@ -750,7 +714,7 @@ fn candidate_terms( replaces_line_id: None, }; let now = now_unix(); - Ok(RadrootsTradeCandidateTermsV1 { + Ok(TradeCandidateTermsV1 { candidate_id: None, schema_version: RADROOTS_TRADE_SCHEMA_VERSION, base_candidate_id: None, @@ -760,7 +724,7 @@ fn candidate_terms( farm_id, lines: vec![line], line_tombstones: Vec::new(), - economics: RadrootsTradeEconomicsProfileV1 { + economics: TradeEconomicsProfileV1 { profile_id: "mvp-fixed".to_owned(), currency_code: product.price_currency.clone(), currency_exponent: u8::try_from(currency_exponent) @@ -772,7 +736,7 @@ fn candidate_terms( total_mantissa: subtotal_mantissa, adjustments: Vec::new(), }, - fulfillment: RadrootsFulfillmentProfileV1 { + fulfillment: FulfillmentProfileV1 { profile_id: "market-pickup".to_owned(), method: "pickup".to_owned(), starts_at_unix_s: now + DEFAULT_FULFILLMENT_START_OFFSET_SECONDS, @@ -785,7 +749,7 @@ fn candidate_terms( location_class: "seller_public_listing".to_owned(), requires_private_terms: false, }, - cancellation: RadrootsTradeCancellationProfileV1 { + cancellation: TradeCancellationProfileV1 { profile_id: "buyer-pre-agreement".to_owned(), buyer_pre_agreement: true, post_agreement_cutoff_unix_s: None, @@ -818,14 +782,14 @@ fn resolve_active_listing_state( "listing address `{listing_addr}` is missing latest listing event state; run `radroots market pull`" )) })?; - RadrootsTradeMutationId::parse(state.content_hash.as_str()).map_err(|error| { + MutationId::parse(state.content_hash.as_str()).map_err(|error| { RuntimeError::Config(format!( "listing content hash is not a 32-byte hex digest: {error}" )) })?; state .last_event_id - .parse::<radroots_event::ids::RadrootsEventId>() + .parse::<radroots_event::id::RadrootsEventId>() .map_err(|error| { RuntimeError::Config(format!("listing latest event id is invalid: {error}")) })?; @@ -835,22 +799,19 @@ fn resolve_active_listing_state( }) } -fn resolve_farm_id( - config: &RuntimeConfig, - seller_pubkey: &str, -) -> Result<RadrootsDTag, RuntimeError> { +fn resolve_farm_id(config: &RuntimeConfig, seller_pubkey: &str) -> Result<DTag, RuntimeError> { let db = ReplicaSql::new(SqlxSqliteExecutor::open(&config.local.replica_store_path)?); let d_tag = db.farm_unique_d_tag_by_pubkey(seller_pubkey)?.ok_or_else(|| { RuntimeError::Config(format!( "seller `{seller_pubkey}` must have exactly one farm profile in the local replica before creating a trade proposal draft" )) })?; - RadrootsDTag::parse(d_tag.as_str()) + DTag::parse(d_tag.as_str()) .map_err(|error| RuntimeError::Config(format!("farm d tag is invalid: {error}"))) } fn parse_listing_addr(raw: &str) -> Result<ParsedListingAddress, RuntimeError> { - let parsed = RadrootsClassifiedListingAddress::parse(raw) + let parsed = ClassifiedListingAddress::parse(raw) .map_err(|error| RuntimeError::Config(format!("listing address is invalid: {error}")))?; let (kind, rest) = parsed .as_str() @@ -869,10 +830,8 @@ fn parse_listing_addr(raw: &str) -> Result<ParsedListingAddress, RuntimeError> { }) } -fn proposal_economics_view( - envelope: &RadrootsTradeMutationEnvelopeV1, -) -> TradeCandidateDraftEconomicsView { - let RadrootsTradeMutationBodyV1::Proposal { candidate } = &envelope.body else { +fn proposal_economics_view(envelope: &TradeMutationEnvelopeV1) -> TradeCandidateDraftEconomicsView { + let TradeMutationBodyV1::Proposal { candidate } = &envelope.body else { unreachable!("proposal draft envelope is a proposal") }; TradeCandidateDraftEconomicsView { @@ -885,8 +844,8 @@ fn proposal_economics_view( } } -fn proposal_candidate_id(envelope: &RadrootsTradeMutationEnvelopeV1) -> Option<String> { - let RadrootsTradeMutationBodyV1::Proposal { candidate } = &envelope.body else { +fn proposal_candidate_id(envelope: &TradeMutationEnvelopeV1) -> Option<String> { + let TradeMutationBodyV1::Proposal { candidate } = &envelope.body else { return None; }; candidate.candidate_id.as_ref().map(ToString::to_string) @@ -953,10 +912,10 @@ fn validate_product_bin(product: &ProductFacts, bin_id: &str) -> Result<(), Runt Ok(()) } -fn exact_positive_decimal(value: &str, field: &str) -> Result<RadrootsCoreDecimal, RuntimeError> { +fn exact_positive_decimal(value: &str, field: &str) -> Result<Decimal, RuntimeError> { let parsed = value .trim() - .parse::<RadrootsCoreDecimal>() + .parse::<Decimal>() .map_err(|error| RuntimeError::Config(format!("listing {field} is invalid: {error}")))?; if parsed.is_zero() || parsed.is_sign_negative() { return Err(RuntimeError::Config(format!( @@ -966,7 +925,7 @@ fn exact_positive_decimal(value: &str, field: &str) -> Result<RadrootsCoreDecima Ok(parsed) } -fn decimal_mantissa_at_scale(mut value: RadrootsCoreDecimal, scale: u32) -> String { +fn decimal_mantissa_at_scale(mut value: Decimal, scale: u32) -> String { value.rescale(scale); value.0.mantissa().to_string() } @@ -979,7 +938,7 @@ fn candidate_draft_file(config: &RuntimeConfig, trade_id: &str) -> PathBuf { .join(format!("{trade_id}.json")) } -fn next_trade_id() -> Result<RadrootsTradeId, RuntimeError> { +fn next_trade_id() -> Result<TradeId, RuntimeError> { let nanos = SystemTime::now() .duration_since(UNIX_EPOCH) .map(|duration| duration.as_nanos()) @@ -996,23 +955,23 @@ fn now_unix() -> u64 { .unwrap_or_default() } -fn trade_id(value: &str, field: &str) -> Result<RadrootsTradeId, RuntimeError> { - RadrootsTradeId::parse(value) +fn trade_id(value: &str, field: &str) -> Result<TradeId, RuntimeError> { + TradeId::parse(value) .map_err(|error| RuntimeError::Config(format!("{field} is invalid: {error}"))) } -fn trade_candidate_id(value: &str, field: &str) -> Result<RadrootsTradeCandidateId, RuntimeError> { - RadrootsTradeCandidateId::parse(value) +fn trade_candidate_id(value: &str, field: &str) -> Result<CandidateId, RuntimeError> { + CandidateId::parse(value) .map_err(|error| RuntimeError::Config(format!("{field} is invalid: {error}"))) } -fn inventory_bin_id(value: &str, field: &str) -> Result<RadrootsInventoryBinId, RuntimeError> { - RadrootsInventoryBinId::parse(value) +fn inventory_bin_id(value: &str, field: &str) -> Result<InventoryBinId, RuntimeError> { + InventoryBinId::parse(value) .map_err(|error| RuntimeError::Config(format!("{field} is invalid: {error}"))) } -fn pubkey(value: &str, field: &str) -> Result<RadrootsPublicKey, RuntimeError> { - RadrootsPublicKey::parse(value) +fn pubkey(value: &str, field: &str) -> Result<PublicKey, RuntimeError> { + PublicKey::parse(value) .map_err(|error| RuntimeError::Config(format!("{field} is invalid: {error}"))) } diff --git a/src/runtime/transport.rs b/src/runtime/transport.rs @@ -545,7 +545,7 @@ fn string_array_input(input: &OperationData, key: &str) -> Vec<String> { #[cfg(test)] mod tests { use super::*; - use radroots_event::ids::RadrootsEventId; + use radroots_event::id::RadrootsEventId; use radroots_sdk::{ PushOutboxEventReceipt, PushOutboxTargetReceipt, PushOutboxTransportOutcomeKind, }; diff --git a/src/view/runtime.rs b/src/view/runtime.rs @@ -1,8 +1,8 @@ use std::process::ExitCode; -use radroots_event::farm::RadrootsFarm; -use radroots_event::ids::RadrootsClassifiedListingAddress; -use radroots_event::operational_listing::RadrootsOperationalListingPublicLocation; +use radroots_event::farm::Farm; +use radroots_event::id::ClassifiedListingAddress; +use radroots_event::listing::operational::OperationalListingPublicLocation; use radroots_nostr_accounts::prelude::RadrootsNostrAccountRecord; use serde::Serialize; @@ -577,8 +577,7 @@ pub struct LocalStatusView { pub sdk_storage: String, pub sdk_root: String, pub sdk_existed_before_open: bool, - pub event_store: SdkEventStoreStatusView, - pub outbox: SdkOutboxStatusView, + pub storage: SdkStorageStatusView, pub integrity: SdkIntegrityView, pub derived_projection: LocalDerivedProjectionStatusView, #[serde(skip_serializing_if = "Option::is_none")] @@ -598,84 +597,22 @@ impl LocalStatusView { } #[derive(Debug, Clone, Serialize)] -pub struct SdkSqliteStatusView { - pub schema_version: i64, - pub journal_mode: String, - pub foreign_keys_enabled: bool, - pub busy_timeout_ms: i64, - pub integrity_ok: bool, - pub integrity_result: String, -} - -#[derive(Debug, Clone, Serialize)] -pub struct SdkEventStoreStatusView { - #[serde(skip_serializing_if = "Option::is_none")] - pub path: Option<String>, - pub store: SdkSqliteStatusView, - pub total_events: i64, - pub valid_stream_events: i64, - pub transport_observations: i64, - #[serde(skip_serializing_if = "Option::is_none")] - pub last_event_seq: Option<i64>, - #[serde(skip_serializing_if = "Option::is_none")] - pub last_event_updated_at_ms: Option<i64>, -} - -#[cfg(test)] -mod sdk_event_store_status_view_tests { - use super::{SdkEventStoreStatusView, SdkSqliteStatusView}; - - #[test] - fn serializes_valid_stream_events_without_legacy_projection_key() { - let value = serde_json::to_value(SdkEventStoreStatusView { - path: None, - store: SdkSqliteStatusView { - schema_version: 1, - journal_mode: "wal".to_owned(), - foreign_keys_enabled: true, - busy_timeout_ms: 5_000, - integrity_ok: true, - integrity_result: "ok".to_owned(), - }, - total_events: 11, - valid_stream_events: 7, - transport_observations: 3, - last_event_seq: Some(11), - last_event_updated_at_ms: Some(1_700_000_000_000), - }) - .expect("event store status view"); - - assert_eq!(value["valid_stream_events"], 7); - assert!(value.get("projection_eligible_events").is_none()); - } -} - -#[derive(Debug, Clone, Serialize)] -pub struct SdkOutboxStatusView { - #[serde(skip_serializing_if = "Option::is_none")] - pub path: Option<String>, - pub store: SdkSqliteStatusView, - pub total_events: i64, - pub pending_events: i64, - pub retryable_events: i64, - pub terminal_events: i64, - pub failed_terminal_events: i64, - pub deferred_until_implemented_events: i64, - pub ready_signed_events: i64, - pub publishing_events: i64, - #[serde(skip_serializing_if = "Option::is_none")] - pub last_attempt_at_ms: Option<i64>, - #[serde(skip_serializing_if = "Option::is_none")] - pub last_error: Option<String>, +pub struct SdkStorageStatusView { + pub backend: String, + pub open_mode: String, + pub writer_policy: String, + pub shutdown: String, + pub wal_enabled: bool, + pub busy_timeout_ms: u32, } #[derive(Debug, Clone, Serialize)] pub struct SdkIntegrityView { - pub checked_paths: Vec<String>, - pub event_store_ok: bool, - pub outbox_ok: bool, - pub event_store_result: String, - pub outbox_result: String, + pub health: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub checked_at_unix_ms: Option<u64>, + pub verified_members: u32, + pub failed_members: u32, } #[derive(Debug, Clone, Serialize)] @@ -1108,7 +1045,7 @@ pub struct FarmConfigSummaryView { pub struct FarmConfigDocumentView { pub selection: FarmSelectionView, pub profile: FarmProfileDraftView, - pub farm: RadrootsFarm, + pub farm: Farm, pub listing_defaults: FarmListingDefaultsView, pub publication: FarmPublicationView, } @@ -1146,7 +1083,7 @@ pub struct FarmSelectionView { #[derive(Debug, Clone, Serialize)] pub struct FarmListingDefaultsView { pub delivery_method: String, - pub location: RadrootsOperationalListingPublicLocation, + pub location: OperationalListingPublicLocation, } #[derive(Debug, Clone, Serialize)] @@ -1235,9 +1172,8 @@ impl MarketReadinessView { price_currency: &str, price_per_amount: f64, ) -> Self { - let protocol_valid = listing_addr.is_some_and(|listing_addr| { - RadrootsClassifiedListingAddress::parse(listing_addr).is_ok() - }); + let protocol_valid = listing_addr + .is_some_and(|listing_addr| ClassifiedListingAddress::parse(listing_addr).is_ok()); let marketplace_eligible = protocol_valid && title.is_some_and(|title| !title.trim().is_empty()) && category.is_some_and(|category| !category.trim().is_empty());