fix(tinyplace): classify wallet-not-configured as expected user-state (#3964) (#3974)

This commit is contained in:
oxoxDev
2026-06-23 11:45:53 -07:00
committed by GitHub
parent 3afb28427d
commit 1d42c766e0
4 changed files with 80 additions and 4 deletions
+31
View File
@@ -103,6 +103,16 @@ pub async fn rpc_handler(State(state): State<AppState>, Json(req): Json<RpcReque
"[rpc] expected-user-state error — skipping Sentry: {}",
display_message
);
} else if is_wallet_not_configured_error(&display_message) {
// A `tinyplace_*` RPC needs a wallet-derived signer but the user
// has not set one up. Expected user-state (the UI shows a
// "set up wallet" prompt), not an internal failure — skip Sentry
// here so the message is left untouched for direct (agent-tool)
// callers. See `is_wallet_not_configured_error`.
tracing::info!(
method = %method,
"[rpc] wallet-not-configured (expected user-state) — skipping Sentry"
);
} else if is_param_validation_error(&display_message) {
tracing::info!(
method = %method,
@@ -347,6 +357,27 @@ fn is_param_validation_error(msg: &str) -> 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
+39 -2
View File
@@ -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"
));
}
+1 -1
View File
@@ -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,
+9 -1
View File
@@ -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<WalletSecretMa
"{LOG_PREFIX} secret_material missing wallet state chain={}",
chain.as_str()
);
return Err("wallet is not configured; run wallet setup first".to_string());
return Err(WALLET_NOT_CONFIGURED_MESSAGE.to_string());
}
};
let encrypted_mnemonic = state