cli

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

commit 705eecaabaa0a83e93de9569e3e1ba789792f424
parent 9c486cdaae18014c682aaf70c563c53eb4c5ea24
Author: triesap <tyson@radroots.org>
Date:   Sat,  4 Jul 2026 01:02:04 +0000

trade: require explicit public note confirmation

- map trade submit directly into product-shaped SDK propose fields
- add confirm-public-note input across reason-bearing trade commands
- remove ambient public-note confirmation from trade privacy preflight
- update parser, operation, and SDK migration guards

Diffstat:
Msrc/cli/global.rs | 5+++++
Msrc/cli/input.rs | 11+++++++++++
Msrc/cli/mod.rs | 6++++++
Msrc/cli/trade.rs | 10++++++++++
Msrc/ops/exec/trade.rs | 14++++++++++++++
Msrc/ops/mod.rs | 27+++++++++++++++++++++++++++
Msrc/runtime/order.rs | 54++++++++++++++++++++++++++++++++++++++----------------
Msrc/runtime/sdk.rs | 4++++
8 files changed, 115 insertions(+), 16 deletions(-)

diff --git a/src/cli/global.rs b/src/cli/global.rs @@ -237,6 +237,7 @@ pub struct OrderDraftAdjustmentArgs { pub struct TradeSubmitArgs { pub key: String, pub idempotency_key: Option<String>, + pub confirm_public_note: bool, } #[derive(Debug, Clone)] @@ -279,6 +280,7 @@ pub struct TradeDecisionArgs { pub decision: TradeDecisionArg, pub reason: Option<String>, pub idempotency_key: Option<String>, + pub confirm_public_note: bool, } #[derive(Debug, Clone)] @@ -286,12 +288,14 @@ pub struct TradeCancelArgs { pub key: String, pub reason: String, pub idempotency_key: Option<String>, + pub confirm_public_note: bool, } #[derive(Debug, Clone)] pub struct TradeRevisionProposeArgs { pub key: String, pub reason: String, + pub confirm_public_note: bool, pub bin_id: Option<String>, pub bin_count: Option<u32>, pub adjustment_id: Option<String>, @@ -331,6 +335,7 @@ pub struct TradeRevisionDecisionArgs { pub decision: TradeRevisionDecisionArg, pub reason: Option<String>, pub idempotency_key: Option<String>, + pub confirm_public_note: bool, } #[derive(Debug, Clone)] diff --git a/src/cli/input.rs b/src/cli/input.rs @@ -248,6 +248,7 @@ pub fn target_operation_input(command: &TargetCommand) -> OperationData { TargetCommand::Trade(args) => match &args.command { TradeCommand::Submit(args) => { insert_string(&mut input, "trade_id", &args.trade_id); + insert_bool(&mut input, "confirm_public_note", args.confirm_public_note); } TradeCommand::Get(args) => insert_string(&mut input, "trade_id", &args.trade_id), TradeCommand::App(args) => match &args.command { @@ -265,15 +266,18 @@ pub fn target_operation_input(command: &TargetCommand) -> OperationData { TradeCommand::Decline(args) => { insert_string(&mut input, "trade_id", &args.trade_id); insert_string(&mut input, "reason", &args.reason); + insert_bool(&mut input, "confirm_public_note", args.confirm_public_note); } TradeCommand::Cancel(args) => { insert_string(&mut input, "trade_id", &args.trade_id); insert_string(&mut input, "reason", &args.reason); + insert_bool(&mut input, "confirm_public_note", args.confirm_public_note); } TradeCommand::Revision(revision) => match &revision.command { TradeRevisionCommand::Propose(args) => { insert_string(&mut input, "trade_id", &args.trade_id); insert_string(&mut input, "reason", &args.reason); + insert_bool(&mut input, "confirm_public_note", args.confirm_public_note); insert_string(&mut input, "bin_id", &args.bin_id); if let Some(bin_count) = args.bin_count { input.insert( @@ -295,6 +299,7 @@ pub fn target_operation_input(command: &TargetCommand) -> OperationData { insert_string(&mut input, "trade_id", &args.trade_id); insert_string(&mut input, "revision_id", &args.revision_id); insert_string(&mut input, "reason", &args.reason); + insert_bool(&mut input, "confirm_public_note", args.confirm_public_note); } }, TradeCommand::Status(status) => match &status.command { @@ -353,6 +358,12 @@ fn insert_number(input: &mut OperationData, key: &str, value: f64) { } } +fn insert_bool(input: &mut OperationData, key: &str, value: bool) { + if value { + input.insert(key.to_owned(), Value::Bool(true)); + } +} + fn insert_path(input: &mut OperationData, key: &str, value: &Option<std::path::PathBuf>) { if let Some(value) = value { input.insert( diff --git a/src/cli/mod.rs b/src/cli/mod.rs @@ -638,6 +638,7 @@ mod tests { "ord_test", "--reason", "changed plans", + "--confirm-public-note", ]) .expect("target args parse"); @@ -650,6 +651,7 @@ mod tests { }; assert_eq!(args.trade_id.as_deref(), Some("ord_test")); assert_eq!(args.reason.as_deref(), Some("changed plans")); + assert!(args.confirm_public_note); } #[test] @@ -676,6 +678,7 @@ mod tests { "USD", "--adjustment-reason", "packing change", + "--confirm-public-note", ]) .expect("target args parse"); @@ -695,6 +698,7 @@ mod tests { assert_eq!(args.bin_count, Some(3)); assert_eq!(args.adjustment_id.as_deref(), Some("adj_revision")); assert_eq!(args.adjustment_effect.as_deref(), Some("increase")); + assert!(args.confirm_public_note); } #[test] @@ -733,6 +737,7 @@ mod tests { "rev_test", "--reason", "keep original trade", + "--confirm-public-note", ]) .expect("target args parse"); @@ -749,6 +754,7 @@ mod tests { assert_eq!(args.trade_id.as_deref(), Some("ord_test")); assert_eq!(args.revision_id.as_deref(), Some("rev_test")); assert_eq!(args.reason.as_deref(), Some("keep original trade")); + assert!(args.confirm_public_note); } #[test] diff --git a/src/cli/trade.rs b/src/cli/trade.rs @@ -26,6 +26,8 @@ pub enum TradeCommand { #[derive(Debug, Clone, Args)] pub struct TradeSubmitArgs { pub trade_id: Option<String>, + #[arg(long)] + pub confirm_public_note: bool, } #[derive(Debug, Clone, Args)] @@ -63,6 +65,8 @@ pub struct TradeDeclineArgs { pub trade_id: Option<String>, #[arg(long)] pub reason: Option<String>, + #[arg(long)] + pub confirm_public_note: bool, } #[derive(Debug, Clone, Args)] @@ -70,6 +74,8 @@ pub struct TradeCancelArgs { pub trade_id: Option<String>, #[arg(long)] pub reason: Option<String>, + #[arg(long)] + pub confirm_public_note: bool, } #[derive(Debug, Clone, Args)] @@ -91,6 +97,8 @@ pub struct TradeRevisionProposeArgs { #[arg(long)] pub reason: Option<String>, #[arg(long)] + pub confirm_public_note: bool, + #[arg(long)] pub bin_id: Option<String>, #[arg(long)] pub bin_count: Option<u32>, @@ -120,6 +128,8 @@ pub struct TradeRevisionDeclineArgs { pub revision_id: Option<String>, #[arg(long)] pub reason: Option<String>, + #[arg(long)] + pub confirm_public_note: bool, } #[derive(Debug, Clone, Args)] diff --git a/src/ops/exec/trade.rs b/src/ops/exec/trade.rs @@ -61,6 +61,7 @@ impl OperationService<TradeSubmitRequest> for TradeOperationService<'_> { .idempotency_key .clone() .or_else(|| string_input(&request, "idempotency_key")), + confirm_public_note: bool_input(&request, "confirm_public_note").unwrap_or(false), }; let mut config = self.config.clone(); if request.context.dry_run { @@ -187,6 +188,7 @@ impl OperationService<TradeAcceptRequest> for TradeOperationService<'_> { .idempotency_key .clone() .or_else(|| string_input(&request, "idempotency_key")), + confirm_public_note: false, }; let mut config = self.config.clone(); if request.context.dry_run { @@ -230,6 +232,7 @@ impl OperationService<TradeDeclineRequest> for TradeOperationService<'_> { .idempotency_key .clone() .or_else(|| string_input(&request, "idempotency_key")), + confirm_public_note: bool_input(&request, "confirm_public_note").unwrap_or(false), }; let mut config = self.config.clone(); if request.context.dry_run { @@ -272,6 +275,7 @@ impl OperationService<TradeCancelRequest> for TradeOperationService<'_> { .idempotency_key .clone() .or_else(|| string_input(&request, "idempotency_key")), + confirm_public_note: bool_input(&request, "confirm_public_note").unwrap_or(false), }; let mut config = self.config.clone(); if request.context.dry_run { @@ -309,6 +313,7 @@ impl OperationService<TradeRevisionProposeRequest> for TradeOperationService<'_> let args = TradeRevisionProposeArgs { key: required_trade_key(&request)?, reason, + confirm_public_note: bool_input(&request, "confirm_public_note").unwrap_or(false), bin_id: string_input(&request, "bin_id"), bin_count: u32_input(&request, "bin_count"), adjustment_id: string_input(&request, "adjustment_id"), @@ -350,6 +355,7 @@ impl OperationService<TradeRevisionAcceptRequest> for TradeOperationService<'_> .idempotency_key .clone() .or_else(|| string_input(&request, "idempotency_key")), + confirm_public_note: false, }; if request.context.requires_approval_token() { return Err(OperationAdapterError::approval_required( @@ -394,6 +400,7 @@ impl OperationService<TradeRevisionDeclineRequest> for TradeOperationService<'_> .idempotency_key .clone() .or_else(|| string_input(&request, "idempotency_key")), + confirm_public_note: bool_input(&request, "confirm_public_note").unwrap_or(false), }; if request.context.requires_approval_token() { return Err(OperationAdapterError::approval_required( @@ -1224,6 +1231,13 @@ where .and_then(|value| u32::try_from(value).ok()) } +fn bool_input<P>(request: &OperationRequest<P>, key: &str) -> Option<bool> +where + P: OperationRequestPayload + OperationRequestData, +{ + request.payload.input().get(key).and_then(Value::as_bool) +} + fn map_runtime<T>(result: Result<T, RuntimeError>) -> Result<T, OperationAdapterError> { result.map_err(|error| OperationAdapterError::Runtime(error.to_string())) } diff --git a/src/ops/mod.rs b/src/ops/mod.rs @@ -302,6 +302,7 @@ mod tests { "USD", "--adjustment-reason", "weather delay", + "--confirm-public-note", ]) .expect("target args parse"); let request = @@ -374,6 +375,14 @@ mod tests { .and_then(Value::as_str), Some("weather delay") ); + assert_eq!( + request + .payload + .input + .get("confirm_public_note") + .and_then(Value::as_bool), + Some(true) + ); let revision_accept = TargetCliArgs::try_parse_from([ "radroots", @@ -418,6 +427,7 @@ mod tests { "rev_test", "--reason", "keep original order", + "--confirm-public-note", ]) .expect("target args parse"); let request = @@ -446,6 +456,14 @@ mod tests { request.payload.input.get("reason").and_then(Value::as_str), Some("keep original order") ); + assert_eq!( + request + .payload + .input + .get("confirm_public_note") + .and_then(Value::as_bool), + Some(true) + ); let cancel = TargetCliArgs::try_parse_from([ "radroots", @@ -454,6 +472,7 @@ mod tests { "ord_test", "--reason", "changed plans", + "--confirm-public-note", ]) .expect("target args parse"); let request = TargetOperationRequest::from_target_args(&cancel).expect("operation request"); @@ -473,6 +492,14 @@ mod tests { request.payload.input.get("reason").and_then(Value::as_str), Some("changed plans") ); + assert_eq!( + request + .payload + .input + .get("confirm_public_note") + .and_then(Value::as_bool), + Some(true) + ); } #[test] diff --git a/src/runtime/order.rs b/src/runtime/order.rs @@ -177,8 +177,13 @@ fn trade_relay_resolution_policy() -> RelayResolutionPolicy { RelayResolutionPolicy::configured_relays() } -fn trade_privacy_confirmation() -> PrivacyPreflightConfirmation { - PrivacyPreflightConfirmation::new().confirm(ProductSensitivityField::PublicButSensitiveNotes) +fn trade_privacy_confirmation(confirm_public_note: bool) -> PrivacyPreflightConfirmation { + if confirm_public_note { + PrivacyPreflightConfirmation::new() + .confirm(ProductSensitivityField::PublicButSensitiveNotes) + } else { + PrivacyPreflightConfirmation::new() + } } fn sdk_trade_actor( @@ -988,12 +993,7 @@ pub fn submit( } let account = validate_bound_order_buyer_account(config, &loaded)?; - let payload = canonical_order_request_payload_from_loaded( - &loaded, - account.record.public_identity.public_key_hex.as_str(), - )?; - - propose_trade_via_sdk(config, &loaded, args, &account, payload) + propose_trade_via_sdk(config, &loaded, args, &account) } pub fn rebind( @@ -1325,7 +1325,7 @@ fn decide_trade_via_sdk( ack_policy, TradeEvidenceMode::ResyncBeforeMutation, ) - .with_privacy_confirmation(trade_privacy_confirmation()); + .with_privacy_confirmation(trade_privacy_confirmation(false)); if let Some(idempotency_key) = args.idempotency_key.as_deref() { request = request.try_with_idempotency_key(idempotency_key)?; } @@ -1349,7 +1349,7 @@ fn decide_trade_via_sdk( ack_policy, TradeEvidenceMode::ResyncBeforeMutation, ) - .with_privacy_confirmation(trade_privacy_confirmation()); + .with_privacy_confirmation(trade_privacy_confirmation(args.confirm_public_note)); if let Some(idempotency_key) = args.idempotency_key.as_deref() { request = request.try_with_idempotency_key(idempotency_key)?; } @@ -1389,7 +1389,7 @@ fn propose_revision_via_sdk( ack_policy, TradeEvidenceMode::ResyncBeforeMutation, ) - .with_privacy_confirmation(trade_privacy_confirmation()); + .with_privacy_confirmation(trade_privacy_confirmation(args.confirm_public_note)); if let Some(idempotency_key) = args.idempotency_key.as_deref() { request = request.try_with_idempotency_key(idempotency_key)?; } @@ -1450,7 +1450,7 @@ fn decide_revision_via_sdk( ack_policy, TradeEvidenceMode::ResyncBeforeMutation, ) - .with_privacy_confirmation(trade_privacy_confirmation()); + .with_privacy_confirmation(trade_privacy_confirmation(args.confirm_public_note)); if let Some(idempotency_key) = args.idempotency_key.as_deref() { request = request.try_with_idempotency_key(idempotency_key)?; } @@ -1497,7 +1497,7 @@ fn cancel_trade_via_sdk( ack_policy, TradeEvidenceMode::ResyncBeforeMutation, ) - .with_privacy_confirmation(trade_privacy_confirmation()); + .with_privacy_confirmation(trade_privacy_confirmation(args.confirm_public_note)); if let Some(idempotency_key) = args.idempotency_key.as_deref() { request = request.try_with_idempotency_key(idempotency_key)?; } @@ -4885,20 +4885,42 @@ fn propose_trade_via_sdk( loaded: &LoadedOrderDraft, args: &TradeSubmitArgs, account: &account::AccountRecordView, - payload: RadrootsOrderRequest, ) -> Result<OrderSubmitView, CliSdkAdapterError> { let actor = sdk_trade_actor(account, RadrootsActorRole::Buyer, "propose")?; let publish_mode = trade_publish_mode(config); let ack_policy = trade_ack_policy(publish_mode)?; + let economics = + loaded.document.order.economics.clone().ok_or_else(|| { + RuntimeError::Config("trade draft is missing quote economics".to_owned()) + })?; + let items = loaded + .document + .order + .items + .iter() + .map(|item| { + Ok(RadrootsOrderItem { + bin_id: protocol_inventory_bin_id(item.bin_id.as_str(), "order item bin_id")?, + bin_count: item.bin_count, + }) + }) + .collect::<Result<Vec<_>, RuntimeError>>()?; let mut request = TradeProposeRequest::new( actor, order_submit_listing_event_ptr(loaded)?, - payload, + protocol_order_id(loaded.document.order.order_id.as_str(), "order_id")?, + protocol_listing_addr(loaded.document.order.listing_addr.as_str(), "listing_addr")?, + protocol_pubkey( + loaded.document.order.seller_pubkey.as_str(), + "seller_pubkey", + )?, + items, + economics, trade_relay_resolution_policy(), publish_mode, ack_policy, ) - .with_privacy_confirmation(trade_privacy_confirmation()); + .with_privacy_confirmation(trade_privacy_confirmation(args.confirm_public_note)); if let Some(idempotency_key) = args.idempotency_key.as_deref() { request = request.try_with_idempotency_key(idempotency_key)?; } diff --git a/src/runtime/sdk.rs b/src/runtime/sdk.rs @@ -991,6 +991,10 @@ mod tests { required_tokens: &[ "propose_trade_via_sdk", "TradeProposeRequest::new", + "protocol_order_id", + "protocol_listing_addr", + "protocol_pubkey", + "args.confirm_public_note", "session.sdk().trades().buyer().propose_trade", "trade_publish_mode(config)", ],