diff --git a/src/core/jsonrpc.rs b/src/core/jsonrpc.rs index 8ec381390..1adcae7ac 100644 --- a/src/core/jsonrpc.rs +++ b/src/core/jsonrpc.rs @@ -103,6 +103,16 @@ pub async fn rpc_handler(State(state): State, Json(req): Json bool { || msg.starts_with("invalid params: ") } +/// Returns `true` when the error is the wallet's "not configured yet" message. +/// +/// Several `tinyplace_*` RPCs derive a signer seed from the wallet before they +/// can run (the feed, signal/messaging, etc. — backend `GraphQLAuth::Agent` +/// requires a signer). For a user who has not set up a wallet, the wallet layer +/// returns [`crate::openhuman::wallet::WALLET_NOT_CONFIGURED_MESSAGE`]. That is +/// an expected user-state, not an internal failure: the UI already renders a +/// "set up wallet" prompt, and there is no local lever to make the call succeed +/// until the user creates a wallet. Classifying it here — at the single Sentry +/// boundary — keeps it out of Sentry for *every* path that surfaces it (the +/// shared client builder and the direct `signal_store` seed call alike) without +/// the controllers returning a structured envelope, which would leak the raw +/// sentinel string to agent tools that call those handlers directly. +/// +/// Matched against the shared wallet constant (exact equality) so a wording +/// change in the wallet layer fails the coupling test in `jsonrpc_tests.rs` +/// rather than silently letting the noise back into Sentry. +fn is_wallet_not_configured_error(msg: &str) -> bool { + msg == crate::openhuman::wallet::WALLET_NOT_CONFIGURED_MESSAGE +} + /// Internal method invocation logic. /// /// It first attempts to match the method name against the static controller diff --git a/src/core/jsonrpc_tests.rs b/src/core/jsonrpc_tests.rs index d75e3e996..fedcdd403 100644 --- a/src/core/jsonrpc_tests.rs +++ b/src/core/jsonrpc_tests.rs @@ -7,8 +7,8 @@ use tokio_util::sync::CancellationToken; use super::{ build_http_schema_dump, default_state, escape_html, invoke_method, is_param_validation_error, - is_session_expired_error, is_unconfirmed_unauthorized_error, params_to_object, - parse_json_params, rpc_handler, type_name, + is_session_expired_error, is_unconfirmed_unauthorized_error, is_wallet_not_configured_error, + params_to_object, parse_json_params, rpc_handler, type_name, }; struct EnvVarGuard { @@ -1407,3 +1407,40 @@ async fn desktop_auth_rejects_embedded_fetch_metadata() { let body = String::from_utf8(body.to_vec()).expect("html body should be utf8"); assert!(body.contains("must be opened as a browser page")); } + +#[test] +fn is_wallet_not_configured_error_matches_wallet_constant() { + // The classifier keys off the wallet layer's exact "not configured" + // message so a wallet-less user's tinyplace RPC stays out of Sentry. + assert!(is_wallet_not_configured_error( + crate::openhuman::wallet::WALLET_NOT_CONFIGURED_MESSAGE + )); +} + +#[test] +fn is_wallet_not_configured_error_is_coupled_to_the_wallet_constant() { + // Drift guard: if the wallet wording changes without updating the shared + // constant the classifier matches, this fails — preventing the noise from + // silently returning to Sentry. Mirrors the param-validation prefix locks. + assert_eq!( + crate::openhuman::wallet::WALLET_NOT_CONFIGURED_MESSAGE, + "wallet is not configured; run wallet setup first" + ); +} + +#[test] +fn is_wallet_not_configured_error_does_not_match_other_errors() { + // Other wallet/seed-derivation failures (decrypt, key derivation, locked + // keychain) are real defects and must keep reaching Sentry. + assert!(!is_wallet_not_configured_error( + "tinyplace signer init: bad seed" + )); + assert!(!is_wallet_not_configured_error( + "decrypt secret: kms timeout" + )); + assert!(!is_wallet_not_configured_error("")); + // Substring-only must not qualify — exact equality is required. + assert!(!is_wallet_not_configured_error( + "rpc failed: wallet is not configured; run wallet setup first" + )); +} diff --git a/src/openhuman/wallet/mod.rs b/src/openhuman/wallet/mod.rs index b9c293fe5..6710f2c98 100644 --- a/src/openhuman/wallet/mod.rs +++ b/src/openhuman/wallet/mod.rs @@ -38,7 +38,7 @@ pub(crate) use execution::{sign_and_broadcast_evm, sign_and_broadcast_solana}; pub(crate) use ops::secret_material; pub use ops::{ reveal_recovery_phrase, setup, status, RevealRecoveryPhraseResult, WalletAccount, WalletChain, - WalletSetupParams, WalletSetupSource, WalletStatus, + WalletSetupParams, WalletSetupSource, WalletStatus, WALLET_NOT_CONFIGURED_MESSAGE, }; pub use schemas::{ all_controller_schemas, all_registered_controllers, all_wallet_controller_schemas, diff --git a/src/openhuman/wallet/ops.rs b/src/openhuman/wallet/ops.rs index 1a110394f..522287774 100644 --- a/src/openhuman/wallet/ops.rs +++ b/src/openhuman/wallet/ops.rs @@ -17,6 +17,14 @@ use crate::rpc::RpcOutcome; const LOG_PREFIX: &str = "[wallet]"; const WALLET_STATE_FILENAME: &str = "wallet-state.json"; +/// Error message returned when the wallet has not been set up yet. +/// +/// This is an expected user-state (the user simply has not created a wallet), +/// not an internal failure. Downstream boundaries that surface this condition +/// — e.g. the `tinyplace` client builder — match against this constant to +/// classify it as `expected_user_state` so it stays out of Sentry. Keep it a +/// shared constant so the producer here and any classifier cannot drift apart. +pub const WALLET_NOT_CONFIGURED_MESSAGE: &str = "wallet is not configured; run wallet setup first"; const VALID_MNEMONIC_WORD_COUNTS: [u8; 5] = [12, 15, 18, 21, 24]; /// Keychain key for the encrypted mnemonic blob (user_id is added by the keyring module). const KEYCHAIN_MNEMONIC_KEY: &str = "wallet.mnemonic"; @@ -738,7 +746,7 @@ pub(crate) async fn secret_material(chain: WalletChain) -> Result