commit c4b37da68337ec0701b605d5e6c979bdd8659b1c
parent 39d3bdc35a8c7a0b750c50fe517a1d12a8919f04
Author: triesap <tyson@radroots.org>
Date: Sun, 10 May 2026 03:00:57 +0000
order: sign with bound buyer account
- validate order submit against the draft buyer actor
- sign order requests with the bound account id
- reject conflicting explicit account overrides
- cover submit failures for missing and watch-only buyers
Diffstat:
2 files changed, 207 insertions(+), 9 deletions(-)
diff --git a/src/runtime/order.rs b/src/runtime/order.rs
@@ -684,6 +684,8 @@ pub fn submit(
});
}
+ validate_bound_order_buyer_account(config, &loaded)?;
+
if let Some(view) = order_submit_listing_freshness_view(config, &loaded, args)? {
return Ok(view);
}
@@ -691,10 +693,7 @@ pub fn submit(
return Ok(view);
}
- let signing = match resolve_local_order_signing_identity(
- config,
- loaded.document.order.buyer_pubkey.as_str(),
- ) {
+ let signing = match resolve_local_order_signing_identity(config, &loaded) {
Ok(signing) => signing,
Err(ActorWriteBindingError::Account(failure)) => return Err(failure.into()),
Err(error) => return Ok(order_binding_error_view(config, &loaded, args, error)),
@@ -10081,16 +10080,154 @@ fn order_binding_error_view(
}
}
+fn validate_bound_order_buyer_account(
+ config: &RuntimeConfig,
+ loaded: &LoadedOrderDraft,
+) -> Result<accounts::AccountRecordView, RuntimeError> {
+ let document = &loaded.document;
+ let account_id = document.buyer_actor.account_id.trim();
+ let buyer_pubkey = document.buyer_actor.pubkey.trim();
+ let snapshot = accounts::snapshot(config)?;
+ let Some(account) = snapshot
+ .accounts
+ .iter()
+ .find(|account| account.record.account_id.as_str() == account_id)
+ .cloned()
+ else {
+ return Err(accounts::AccountRuntimeFailure::unresolved_with_detail(
+ format!(
+ "order-bound buyer account `{account_id}` is not present in the local account store"
+ ),
+ order_buyer_failure_detail(
+ loaded,
+ json!({
+ "actions": [
+ "radroots account import <path>",
+ format!("radroots order rebind {} <selector>", document.order.order_id),
+ format!("radroots order get {}", document.order.order_id),
+ ],
+ }),
+ ),
+ )
+ .into());
+ };
+
+ let account_pubkey = account.record.public_identity.public_key_hex.as_str();
+ if !account_pubkey.eq_ignore_ascii_case(buyer_pubkey)
+ || !document
+ .order
+ .buyer_pubkey
+ .eq_ignore_ascii_case(buyer_pubkey)
+ {
+ return Err(accounts::AccountRuntimeFailure::mismatch_with_detail(
+ format!(
+ "order-bound buyer account `{account_id}` does not match order buyer pubkey `{buyer_pubkey}`"
+ ),
+ order_buyer_failure_detail(
+ loaded,
+ json!({
+ "attempted_buyer_account_id": account_id,
+ "attempted_buyer_pubkey": account_pubkey,
+ "actions": [
+ format!("radroots order rebind {} <selector>", document.order.order_id),
+ format!("radroots order get {}", document.order.order_id),
+ ],
+ }),
+ ),
+ )
+ .into());
+ }
+
+ if !account.write_capable {
+ return Err(accounts::AccountRuntimeFailure::watch_only_with_detail(
+ account_id,
+ order_buyer_failure_detail(
+ loaded,
+ json!({
+ "actions": [
+ format!("radroots account attach-secret {account_id} <path>"),
+ format!("radroots order get {}", document.order.order_id),
+ ],
+ }),
+ ),
+ )
+ .into());
+ }
+
+ if let Some(selector) = config.account.selector.as_deref() {
+ let attempted = accounts::resolve_account_selector(config, selector).map_err(|_| {
+ accounts::AccountRuntimeFailure::unresolved_with_detail(
+ format!("account override `{selector}` did not resolve to a local buyer account"),
+ order_buyer_failure_detail(
+ loaded,
+ json!({
+ "attempted_buyer_account_id": selector,
+ "actions": [
+ "radroots account list",
+ format!("radroots order get {}", document.order.order_id),
+ ],
+ }),
+ ),
+ )
+ })?;
+ if attempted.record.account_id.as_str() != account_id {
+ let attempted_pubkey = attempted.record.public_identity.public_key_hex.as_str();
+ return Err(accounts::AccountRuntimeFailure::mismatch_with_detail(
+ format!(
+ "account override `{}` cannot retarget order `{}` bound to buyer account `{account_id}`",
+ attempted.record.account_id, document.order.order_id
+ ),
+ order_buyer_failure_detail(
+ loaded,
+ json!({
+ "attempted_buyer_account_id": attempted.record.account_id.to_string(),
+ "attempted_buyer_pubkey": attempted_pubkey,
+ "actions": [
+ format!("radroots --account-id {account_id} order submit {}", document.order.order_id),
+ format!("radroots order rebind {} <selector>", document.order.order_id),
+ format!("radroots order get {}", document.order.order_id),
+ ],
+ }),
+ ),
+ )
+ .into());
+ }
+ }
+
+ Ok(account)
+}
+
+fn order_buyer_failure_detail(
+ loaded: &LoadedOrderDraft,
+ mut extra: serde_json::Value,
+) -> serde_json::Value {
+ let mut detail = json!({
+ "buyer_actor_source": loaded.document.buyer_actor.source.as_str(),
+ "order_buyer_account_id": loaded.document.buyer_actor.account_id.as_str(),
+ "order_buyer_pubkey": loaded.document.buyer_actor.pubkey.as_str(),
+ "order_file": loaded.file.display().to_string(),
+ "order_id": loaded.document.order.order_id.as_str(),
+ });
+ if let (Some(detail), Some(extra)) = (detail.as_object_mut(), extra.as_object_mut()) {
+ for (key, value) in std::mem::take(extra) {
+ detail.insert(key, value);
+ }
+ }
+ detail
+}
+
fn resolve_local_order_signing_identity(
config: &RuntimeConfig,
- buyer_pubkey: &str,
+ loaded: &LoadedOrderDraft,
) -> Result<accounts::AccountSigningIdentity, ActorWriteBindingError> {
if !matches!(config.signer.backend, SignerBackend::Local) {
return Err(ActorWriteBindingError::Unconfigured(
"order submit requires signer mode `local`".to_owned(),
));
}
- let signing = accounts::resolve_local_signing_identity(config)
+ let account_id = loaded.document.buyer_actor.account_id.trim();
+ let buyer_pubkey = loaded.document.buyer_actor.pubkey.trim();
+ let signing = accounts::resolve_local_signing_identity_for_account(config, account_id)
.map_err(ActorWriteBindingError::from_runtime)?;
let selected_pubkey = signing
.account
@@ -10100,9 +10237,22 @@ fn resolve_local_order_signing_identity(
.as_str();
if !selected_pubkey.eq_ignore_ascii_case(buyer_pubkey) {
return Err(ActorWriteBindingError::Account(
- accounts::AccountRuntimeFailure::mismatch(format!(
- "account mismatch: resolved account pubkey `{selected_pubkey}` cannot sign order buyer_pubkey `{buyer_pubkey}`"
- )),
+ accounts::AccountRuntimeFailure::mismatch_with_detail(
+ format!(
+ "account mismatch: order-bound buyer account `{account_id}` pubkey `{selected_pubkey}` cannot sign order buyer_pubkey `{buyer_pubkey}`"
+ ),
+ order_buyer_failure_detail(
+ loaded,
+ json!({
+ "attempted_buyer_account_id": signing.account.record.account_id.to_string(),
+ "attempted_buyer_pubkey": selected_pubkey,
+ "actions": [
+ format!("radroots order rebind {} <selector>", loaded.document.order.order_id),
+ format!("radroots order get {}", loaded.document.order.order_id),
+ ],
+ }),
+ ),
+ ),
));
}
Ok(signing)
diff --git a/tests/target_cli.rs b/tests/target_cli.rs
@@ -4126,6 +4126,20 @@ fn order_get_and_list_report_missing_bound_buyer_account() {
.iter()
.any(|issue| issue["code"] == "account_unresolved")
);
+
+ let (submit_output, submit) = sandbox.json_output(&[
+ "--format",
+ "json",
+ "--dry-run",
+ "order",
+ "submit",
+ order_id.as_str(),
+ ]);
+ assert!(!submit_output.status.success());
+ assert_eq!(submit_output.status.code(), Some(5));
+ assert_eq!(submit["operation_id"], "order.submit");
+ assert_eq!(submit["errors"][0]["code"], "account_unresolved");
+ assert_eq!(submit["errors"][0]["detail"]["order_id"], order_id);
}
#[test]
@@ -4200,6 +4214,17 @@ fn order_get_marks_watch_only_bound_buyer_unready() {
"radroots account attach-secret {account_id} <path>"
)))
);
+
+ let (submit_output, submit) =
+ sandbox.json_output(&["--format", "json", "--dry-run", "order", "submit", order_id]);
+ assert!(!submit_output.status.success());
+ assert_eq!(submit_output.status.code(), Some(7));
+ assert_eq!(submit["operation_id"], "order.submit");
+ assert_eq!(submit["errors"][0]["code"], "account_watch_only");
+ assert_eq!(
+ submit["errors"][0]["detail"]["order_buyer_account_id"],
+ account_id
+ );
}
#[test]
@@ -4577,6 +4602,29 @@ fn ready_order_submit_dry_run_validates_local_buyer_authority() {
let second_account_id = second["result"]["account"]["id"]
.as_str()
.expect("second account id");
+ sandbox.json_success(&[
+ "--format",
+ "json",
+ "account",
+ "selection",
+ "update",
+ second_account_id,
+ ]);
+ let (drift_output, drift) =
+ sandbox.json_output(&["--format", "json", "--dry-run", "order", "submit", order_id]);
+ assert!(!drift_output.status.success());
+ assert_eq!(drift_output.status.code(), Some(8));
+ assert_eq!(drift["operation_id"], "order.submit");
+ assert_eq!(drift["errors"][0]["code"], "network_unavailable");
+ assert!(
+ drift["errors"][0]["message"]
+ .as_str()
+ .expect("message")
+ .contains(
+ "order submit requires at least one configured relay before publish preflight"
+ )
+ );
+
let (output, mismatch) = sandbox.json_output(&[
"--format",
"json",