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:
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)",
],