commit e23ea523c84b190abb9b80ad2821e99b801ff3c2
parent 9d8004ea1dad724340952ecfe0e3e02d073fd6e5
Author: triesap <tyson@radroots.org>
Date: Mon, 27 Apr 2026 07:23:29 +0000
cli: classify account binding mismatch
Diffstat:
2 files changed, 77 insertions(+), 0 deletions(-)
diff --git a/src/operation_adapter.rs b/src/operation_adapter.rs
@@ -324,6 +324,11 @@ pub enum OperationAdapterError {
operation_id: String,
message: String,
},
+ #[error("account mismatch for `{operation_id}`: {message}")]
+ AccountMismatch {
+ operation_id: String,
+ message: String,
+ },
#[error("signer unconfigured for `{operation_id}`: {message}")]
SignerUnconfigured {
operation_id: String,
@@ -432,6 +437,16 @@ impl OperationAdapterError {
message,
CliExitCode::SignerUnavailable,
),
+ Self::AccountMismatch {
+ operation_id,
+ message,
+ } => runtime_output_error(
+ "account_mismatch",
+ operation_id,
+ "account",
+ message,
+ CliExitCode::AuthorizationFailed,
+ ),
Self::SignerUnconfigured {
operation_id,
message,
@@ -535,6 +550,19 @@ fn classify_runtime_failure(
if contains_any(
&lowered,
&[
+ "account mismatch",
+ "selected local account",
+ "cannot sign listing seller_pubkey",
+ ],
+ ) {
+ return OperationAdapterError::AccountMismatch {
+ operation_id: operation_id.to_owned(),
+ message,
+ };
+ }
+ if contains_any(
+ &lowered,
+ &[
"no account",
"no local account",
"account selector",
@@ -1325,6 +1353,16 @@ mod tests {
(
OperationAdapterError::unconfigured(
"listing.publish",
+ "selected local account pubkey `b` cannot sign listing seller_pubkey `a`"
+ .to_owned(),
+ ),
+ "account_mismatch",
+ "account",
+ 5,
+ ),
+ (
+ OperationAdapterError::unconfigured(
+ "listing.publish",
"signer.remote_nip46 binding is missing".to_owned(),
),
"signer_unconfigured",
diff --git a/tests/signer_runtime_modes.rs b/tests/signer_runtime_modes.rs
@@ -342,6 +342,45 @@ fn local_listing_publish_signs_with_selected_account_without_remote_fallback() {
}
#[test]
+fn local_listing_publish_fails_when_selected_account_does_not_match_seller() {
+ let sandbox = RadrootsCliSandbox::new();
+ let first = sandbox.json_success(&["--format", "json", "account", "create"]);
+ let first_account_id = first["result"]["account"]["id"]
+ .as_str()
+ .expect("first account id");
+ let listing_file = create_listing_draft(&sandbox, "local-mismatch");
+ make_listing_publishable(&listing_file, "AAAAAAAAAAAAAAAAAAAAAw");
+ let second = sandbox.json_success(&["--format", "json", "account", "create"]);
+ let second_account_id = second["result"]["account"]["id"]
+ .as_str()
+ .expect("second account id");
+ assert_ne!(first_account_id, second_account_id);
+
+ let (output, value) = sandbox.json_output(&[
+ "--format",
+ "json",
+ "--account-id",
+ second_account_id,
+ "--approval-token",
+ "approve",
+ "listing",
+ "publish",
+ listing_file.to_string_lossy().as_ref(),
+ ]);
+
+ assert!(!output.status.success());
+ assert_eq!(value["operation_id"], "listing.publish");
+ assert_eq!(value["result"], serde_json::Value::Null);
+ assert_eq!(value["errors"][0]["code"], "account_mismatch");
+ assert_eq!(value["errors"][0]["exit_code"], 5);
+ assert_eq!(value["errors"][0]["detail"]["class"], "account");
+ assert_contains(
+ &value["errors"][0]["message"],
+ "cannot sign listing seller_pubkey",
+ );
+}
+
+#[test]
fn watch_only_listing_publish_fails_as_account_watch_only() {
let sandbox = RadrootsCliSandbox::new();
let public_identity = identity_public(12);