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:
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());