commit 48900011c281245c96d11c2acff1253be9dc12b3
parent ae793686fc4136d3432ca0b496c24f7cdf035e49
Author: triesap <tyson@radroots.org>
Date: Tue, 5 May 2026 17:02:42 +0000
cli: fix post-pricing review drift
- classify revision commands as relay-backed network operations
- build signed order economics from exact replica listing values
- remove unused production warning surfaces from the cli runtime
- add target tests for revision posture and exact economics
Diffstat:
6 files changed, 265 insertions(+), 56 deletions(-)
diff --git a/src/main.rs b/src/main.rs
@@ -389,7 +389,10 @@ fn validate_network_contract(
Ok(())
}
OperationNetworkMode::Online => {
- if external && !request.context().dry_run && config.relay.urls.is_empty() {
+ if external
+ && (!request.context().dry_run || dry_run_requires_network(request.operation_id()))
+ && config.relay.urls.is_empty()
+ {
return Err(OperationAdapterError::NetworkUnavailable {
operation_id: spec.operation_id.to_owned(),
message: format!(
@@ -409,6 +412,9 @@ fn dry_run_requires_network(operation_id: &str) -> bool {
"order.accept"
| "order.decline"
| "order.cancel"
+ | "order.revision.propose"
+ | "order.revision.accept"
+ | "order.revision.decline"
| "order.fulfillment.update"
| "order.receipt.record"
)
@@ -428,6 +434,9 @@ fn external_network_operation(operation_id: &str) -> bool {
| "order.accept"
| "order.decline"
| "order.cancel"
+ | "order.revision.propose"
+ | "order.revision.accept"
+ | "order.revision.decline"
| "order.fulfillment.update"
| "order.receipt.record"
| "order.status.get"
diff --git a/src/runtime/accounts.rs b/src/runtime/accounts.rs
@@ -36,8 +36,6 @@ pub struct AccountRecordView {
#[derive(Debug, Clone)]
pub struct AccountSecretBackendStatus {
- pub configured_primary: String,
- pub configured_fallback: Option<String>,
pub state: String,
pub active_backend: Option<String>,
pub used_fallback: bool,
@@ -380,32 +378,20 @@ pub fn unresolved_account_reason(config: &RuntimeConfig) -> Result<String, Runti
}
pub fn secret_backend_status(config: &RuntimeConfig) -> AccountSecretBackendStatus {
- let configured_primary = config.account.secret_backend.kind().to_string();
- let configured_fallback = config
- .account
- .secret_fallback
- .map(|backend| backend.kind().to_string());
-
match resolve_secret_backend(config) {
Ok(resolved) => AccountSecretBackendStatus {
- configured_primary,
- configured_fallback,
state: "ready".to_owned(),
active_backend: Some(resolved.backend.kind().to_string()),
used_fallback: resolved.used_fallback,
reason: None,
},
Err(SecretBackendResolutionError::Unavailable(reason)) => AccountSecretBackendStatus {
- configured_primary,
- configured_fallback,
state: "unavailable".to_owned(),
active_backend: None,
used_fallback: false,
reason: Some(reason),
},
Err(SecretBackendResolutionError::Invalid(reason)) => AccountSecretBackendStatus {
- configured_primary,
- configured_fallback,
state: "error".to_owned(),
active_backend: None,
used_fallback: false,
diff --git a/src/runtime/config.rs b/src/runtime/config.rs
@@ -247,6 +247,7 @@ pub enum CapabilityBindingTargetKind {
ExplicitEndpoint,
}
+#[cfg(test)]
impl CapabilityBindingTargetKind {
pub fn as_str(self) -> &'static str {
match self {
@@ -262,6 +263,7 @@ pub enum CapabilityBindingSource {
WorkspaceConfig,
}
+#[cfg(test)]
impl CapabilityBindingSource {
pub fn as_str(self) -> &'static str {
match self {
@@ -283,6 +285,7 @@ pub struct CapabilityBindingConfig {
pub source: CapabilityBindingSource,
}
+#[cfg(test)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CapabilityBindingInspectionState {
Configured,
@@ -290,16 +293,7 @@ pub enum CapabilityBindingInspectionState {
Disabled,
}
-impl CapabilityBindingInspectionState {
- pub fn as_str(self) -> &'static str {
- match self {
- Self::Configured => "configured",
- Self::NotConfigured => "not_configured",
- Self::Disabled => "disabled",
- }
- }
-}
-
+#[cfg(test)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CapabilityBindingInspection {
pub capability_id: String,
@@ -408,7 +402,6 @@ struct CapabilityBindingSpec {
}
pub(crate) const SIGNER_REMOTE_NIP46_CAPABILITY: &str = "signer.remote_nip46";
-pub(crate) const WORKFLOW_TRADE_CAPABILITY: &str = "workflow.trade";
pub(crate) const INFERENCE_HYF_STDIO_CAPABILITY: &str = "inference.hyf_stdio";
const CAPABILITY_BINDING_SPECS: &[CapabilityBindingSpec] = &[
@@ -618,6 +611,7 @@ impl RuntimeConfig {
})
}
+ #[cfg(test)]
pub fn inspect_capability_bindings(&self) -> Vec<CapabilityBindingInspection> {
CAPABILITY_BINDING_SPECS
.iter()
diff --git a/src/runtime/mod.rs b/src/runtime/mod.rs
@@ -11,6 +11,7 @@ pub mod logging;
pub mod network;
pub mod order;
pub mod paths;
+#[cfg(test)]
pub mod provider;
pub mod signer;
pub mod sync;
diff --git a/src/runtime/order.rs b/src/runtime/order.rs
@@ -164,11 +164,11 @@ struct ResolvedOrderListing {
#[derive(Debug, Clone)]
struct ResolvedOrderEconomicsProduct {
- qty_amt: i64,
+ qty_amt_exact: Option<String>,
qty_unit: String,
- price_amt: f64,
+ price_amt_exact: Option<String>,
price_currency: String,
- price_qty_amt: u32,
+ price_qty_amt_exact: Option<String>,
price_qty_unit: String,
primary_bin_id: Option<String>,
notes: Option<String>,
@@ -177,11 +177,11 @@ struct ResolvedOrderEconomicsProduct {
impl ResolvedOrderEconomicsProduct {
fn from_summary(row: &ReplicaTradeProductSummaryRow) -> Self {
Self {
- qty_amt: row.qty_amt,
+ qty_amt_exact: row.qty_amt_exact.clone(),
qty_unit: row.qty_unit.clone(),
- price_amt: row.price_amt,
+ price_amt_exact: row.price_amt_exact.clone(),
price_currency: row.price_currency.clone(),
- price_qty_amt: row.price_qty_amt,
+ price_qty_amt_exact: row.price_qty_amt_exact.clone(),
price_qty_unit: row.price_qty_unit.clone(),
primary_bin_id: row.primary_bin_id.clone(),
notes: row.notes.clone(),
@@ -190,11 +190,11 @@ impl ResolvedOrderEconomicsProduct {
fn from_product(row: TradeProduct) -> Self {
Self {
- qty_amt: row.qty_amt,
+ qty_amt_exact: row.qty_amt_exact,
qty_unit: row.qty_unit,
- price_amt: row.price_amt,
+ price_amt_exact: row.price_amt_exact,
price_currency: row.price_currency,
- price_qty_amt: row.price_qty_amt,
+ price_qty_amt_exact: row.price_qty_amt_exact,
price_qty_unit: row.price_qty_unit,
primary_bin_id: row.primary_bin_id,
notes: row.notes,
@@ -1608,6 +1608,7 @@ struct OrderStatusContext<'a> {
selected_account_pubkey: Option<&'a str>,
}
+#[cfg(test)]
fn order_status_from_receipt(order_id: &str, receipt: DirectRelayFetchReceipt) -> OrderStatusView {
order_status_from_receipt_with_context(
OrderStatusContext {
@@ -6813,12 +6814,15 @@ fn trade_product_listing_addr_filter(listing_addr: &str) -> ITradeProductFieldsF
profile: None,
year: None,
qty_amt: None,
+ qty_amt_exact: None,
qty_unit: None,
qty_label: None,
qty_avail: None,
price_amt: None,
+ price_amt_exact: None,
price_currency: None,
price_qty_amt: None,
+ price_qty_amt_exact: None,
price_qty_unit: None,
listing_addr: Some(listing_addr.to_owned()),
primary_bin_id: None,
@@ -6850,16 +6854,15 @@ fn order_economics_from_resolved_listing(
}
let currency = parse_economics_currency(product.price_currency.as_str(), "price_currency")?;
- let quantity_amount = decimal_from_non_negative_i64(product.qty_amt, "qty_amt")?;
+ let quantity_amount =
+ exact_non_negative_decimal(product.qty_amt_exact.as_deref(), "qty_amt_exact")?;
let quantity_unit = parse_economics_unit(product.qty_unit.as_str(), "qty_unit")?;
- let price_amount = decimal_from_non_negative_f64(product.price_amt, "price_amt")?;
- let price_quantity_amount = if product.price_qty_amt == 0 {
- return Err(RuntimeError::Config(
- "listing price_qty_amt must be greater than zero".to_owned(),
- ));
- } else {
- RadrootsCoreDecimal::from(product.price_qty_amt)
- };
+ let price_amount =
+ exact_non_negative_decimal(product.price_amt_exact.as_deref(), "price_amt_exact")?;
+ let price_quantity_amount = exact_positive_decimal(
+ product.price_qty_amt_exact.as_deref(),
+ "price_qty_amt_exact",
+ )?;
let price_unit = parse_economics_unit(product.price_qty_unit.as_str(), "price_qty_unit")?;
let quantity_unit_in_price_units =
convert_unit_decimal(RadrootsCoreDecimal::ONE, quantity_unit, price_unit).map_err(
@@ -7063,29 +7066,39 @@ fn parse_economics_unit(value: &str, field: &str) -> Result<RadrootsCoreUnit, Ru
.map_err(|error| RuntimeError::Config(format!("listing {field} is invalid: {error}")))
}
-fn decimal_from_non_negative_i64(
- value: i64,
+fn exact_non_negative_decimal(
+ value: Option<&str>,
field: &str,
) -> Result<RadrootsCoreDecimal, RuntimeError> {
- if value < 0 {
+ let parsed = exact_decimal(value, field)?;
+ if parsed.is_sign_negative() {
return Err(RuntimeError::Config(format!(
"listing {field} must be non-negative"
)));
}
- Ok(RadrootsCoreDecimal::from(value))
+ Ok(parsed)
}
-fn decimal_from_non_negative_f64(
- value: f64,
+fn exact_positive_decimal(
+ value: Option<&str>,
field: &str,
) -> Result<RadrootsCoreDecimal, RuntimeError> {
- if !value.is_finite() || value < 0.0 {
+ let parsed = exact_non_negative_decimal(value, field)?;
+ if parsed.is_zero() {
return Err(RuntimeError::Config(format!(
- "listing {field} must be a finite non-negative decimal"
+ "listing {field} must be greater than zero"
)));
}
+ Ok(parsed)
+}
+
+fn exact_decimal(value: Option<&str>, field: &str) -> Result<RadrootsCoreDecimal, RuntimeError> {
+ let Some(value) = value.and_then(non_empty_ref) else {
+ return Err(RuntimeError::Config(format!(
+ "listing {field} exact source is missing"
+ )));
+ };
value
- .to_string()
.parse::<RadrootsCoreDecimal>()
.map_err(|error| RuntimeError::Config(format!("listing {field} is invalid: {error}")))
}
@@ -8704,11 +8717,11 @@ mod tests {
listing_event_id: "1".repeat(64),
seller_pubkey: "seller".to_owned(),
economics_product: Some(ResolvedOrderEconomicsProduct {
- qty_amt: 1,
+ qty_amt_exact: Some("1".to_owned()),
qty_unit: "each".to_owned(),
- price_amt: 10.0,
+ price_amt_exact: Some("10".to_owned()),
price_currency: "USD".to_owned(),
- price_qty_amt: 1,
+ price_qty_amt_exact: Some("1".to_owned()),
price_qty_unit: "each".to_owned(),
primary_bin_id: Some("bin-1".to_owned()),
notes: Some(
@@ -8772,6 +8785,46 @@ mod tests {
}
#[test]
+ fn order_economics_uses_exact_listing_values_over_display_projection() {
+ let listing = ResolvedOrderListing {
+ listing_addr: "30402:seller:AAAAAAAAAAAAAAAAAAAAAg".to_owned(),
+ listing_event_id: "1".repeat(64),
+ seller_pubkey: "seller".to_owned(),
+ economics_product: Some(ResolvedOrderEconomicsProduct {
+ qty_amt_exact: Some("0.5".to_owned()),
+ qty_unit: "each".to_owned(),
+ price_amt_exact: Some("10.25".to_owned()),
+ price_currency: "USD".to_owned(),
+ price_qty_amt_exact: Some("1".to_owned()),
+ price_qty_unit: "each".to_owned(),
+ primary_bin_id: Some("bin-1".to_owned()),
+ notes: None,
+ }),
+ };
+ let items = vec![OrderDraftItem {
+ bin_id: "bin-1".to_owned(),
+ bin_count: 2,
+ }];
+
+ let economics = order_economics_from_resolved_listing(
+ "ord_AAAAAAAAAAAAAAAAAAAAAg",
+ Some(&listing),
+ items.as_slice(),
+ &[],
+ )
+ .expect("economics")
+ .expect("economics present");
+
+ assert_eq!(
+ economics.subtotal,
+ RadrootsCoreMoney::new(
+ "10.25".parse::<RadrootsCoreDecimal>().unwrap(),
+ RadrootsCoreCurrency::USD
+ )
+ );
+ }
+
+ #[test]
fn order_draft_requires_listing_event_id_for_submit_readiness() {
let document = OrderDraftDocument {
version: 1,
diff --git a/tests/target_cli.rs b/tests/target_cli.rs
@@ -519,6 +519,63 @@ fn offline_forbids_external_network_operations() {
.as_slice(),
),
(
+ "order.revision.propose",
+ [
+ "--format",
+ "json",
+ "--offline",
+ "--approval-token",
+ "approve",
+ "order",
+ "revision",
+ "propose",
+ "ord_offline_revision",
+ "--reason",
+ "update count",
+ "--bin-id",
+ "bin-1",
+ "--bin-count",
+ "2",
+ ]
+ .as_slice(),
+ ),
+ (
+ "order.revision.accept",
+ [
+ "--format",
+ "json",
+ "--offline",
+ "--approval-token",
+ "approve",
+ "order",
+ "revision",
+ "accept",
+ "ord_offline_revision",
+ "--revision-id",
+ "revision_1",
+ ]
+ .as_slice(),
+ ),
+ (
+ "order.revision.decline",
+ [
+ "--format",
+ "json",
+ "--offline",
+ "--approval-token",
+ "approve",
+ "order",
+ "revision",
+ "decline",
+ "ord_offline_revision",
+ "--revision-id",
+ "revision_1",
+ "--reason",
+ "keep original",
+ ]
+ .as_slice(),
+ ),
+ (
"order.fulfillment.update",
[
"--format",
@@ -631,6 +688,60 @@ fn offline_rejects_order_decision_dry_run() {
.as_slice(),
),
(
+ "order.revision.propose",
+ [
+ "--format",
+ "json",
+ "--offline",
+ "--dry-run",
+ "order",
+ "revision",
+ "propose",
+ "ord_offline_revision",
+ "--reason",
+ "update count",
+ "--bin-id",
+ "bin-1",
+ "--bin-count",
+ "2",
+ ]
+ .as_slice(),
+ ),
+ (
+ "order.revision.accept",
+ [
+ "--format",
+ "json",
+ "--offline",
+ "--dry-run",
+ "order",
+ "revision",
+ "accept",
+ "ord_offline_revision",
+ "--revision-id",
+ "revision_1",
+ ]
+ .as_slice(),
+ ),
+ (
+ "order.revision.decline",
+ [
+ "--format",
+ "json",
+ "--offline",
+ "--dry-run",
+ "order",
+ "revision",
+ "decline",
+ "ord_offline_revision",
+ "--revision-id",
+ "revision_1",
+ "--reason",
+ "keep original",
+ ]
+ .as_slice(),
+ ),
+ (
"order.fulfillment.update",
[
"--format",
@@ -776,6 +887,61 @@ fn online_requires_relay_for_external_network_operations() {
.as_slice(),
),
(
+ "order.revision.propose",
+ [
+ "--format",
+ "json",
+ "--online",
+ "--approval-token",
+ "approve",
+ "order",
+ "revision",
+ "propose",
+ "ord_missing",
+ "--reason",
+ "update count",
+ "--bin-id",
+ "bin-1",
+ "--bin-count",
+ "2",
+ ]
+ .as_slice(),
+ ),
+ (
+ "order.revision.accept",
+ [
+ "--format",
+ "json",
+ "--online",
+ "--dry-run",
+ "order",
+ "revision",
+ "accept",
+ "ord_missing",
+ "--revision-id",
+ "revision_1",
+ ]
+ .as_slice(),
+ ),
+ (
+ "order.revision.decline",
+ [
+ "--format",
+ "json",
+ "--online",
+ "--dry-run",
+ "order",
+ "revision",
+ "decline",
+ "ord_missing",
+ "--revision-id",
+ "revision_1",
+ "--reason",
+ "keep original",
+ ]
+ .as_slice(),
+ ),
+ (
"order.fulfillment.update",
[
"--format",