From 586d29efe7d5434d995abc42be7bcd1a25c0440a Mon Sep 17 00:00:00 2001 From: jwinterm Date: Sun, 19 Jul 2026 03:44:47 -0400 Subject: [PATCH] chore: cargo fmt with current stable rustfmt (repo-wide) The repo pins channel=stable (no rustfmt version), so CI now pulls rustfmt 1.9.0, which reformats code last touched by an older rustfmt. main itself failed `cargo fmt --check` under the current toolchain; this is a formatting-only sweep across 24 files to make the fmt gate green. No behavior change; tests + clippy -D warnings still pass. --- src/api/admin.rs | 47 ++- src/api/auth.rs | 60 +++- src/api/landing.rs | 6 +- src/api/tips.rs | 17 +- src/config.rs | 29 +- src/config_overlay.rs | 21 +- src/infra/db/operator_settings.rs | 8 +- src/infra/db/social_tips.rs | 12 +- src/infra/db/users.rs | 5 +- src/infra/electrum.rs | 8 +- src/infra/grin/mod.rs | 4 +- src/infra/grin_lws/client.rs | 5 +- src/infra/lws/client.rs | 3 +- src/infra/prices.rs | 13 +- src/infra/relay/nauthz.rs | 13 +- src/infra/relay/policy.rs | 22 +- src/models/tip_status.rs | 5 +- src/tips/confirmation.rs | 47 ++- src/tips/gc.rs | 5 +- src/tips/sweep_reconciler.rs | 93 +++-- tests/common/mod.rs | 15 +- tests/nostr_register.rs | 13 +- tests/registration_atomicity.rs | 17 +- tests/tips.rs | 550 +++++++++++++++++++++++++----- 24 files changed, 785 insertions(+), 233 deletions(-) diff --git a/src/api/admin.rs b/src/api/admin.rs index 55adc8d..a422017 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -838,15 +838,18 @@ pub async fn admin_config_put( let mut merged_json = serde_json::to_value(&persisted).map_err(|e| AppError::Internal(e.to_string()))?; json_merge_skip_null(&mut merged_json, &patch_json); - let merged: crate::config_overlay::SettingsOverlay = serde_json::from_value(merged_json.clone()) - .map_err(|e| AppError::ValidationError(format!("bad settings: {e}")))?; + let merged: crate::config_overlay::SettingsOverlay = + serde_json::from_value(merged_json.clone()) + .map_err(|e| AppError::ValidationError(format!("bad settings: {e}")))?; let candidate = state.config_base.apply_overlay(&merged)?; // ValidationError -> 400 // Classify the CHANGED fields: which sections changed + does any need a restart? let mut any_restart = false; let mut changed_sections: Vec = Vec::new(); for (section, fields) in &patch_obj { - let Some(fields) = fields.as_object() else { continue }; + let Some(fields) = fields.as_object() else { + continue; + }; let touched: Vec<&String> = fields .iter() .filter(|(_, v)| !v.is_null()) @@ -907,7 +910,9 @@ pub async fn admin_config_put( let shutdown = state.shutdown.clone(); tokio::spawn(async move { tokio::time::sleep(std::time::Duration::from_millis(750)).await; - tracing::info!("config auto-apply: self-restart to apply a restart-required change"); + tracing::info!( + "config auto-apply: self-restart to apply a restart-required change" + ); shutdown.notify_one(); }); } @@ -933,10 +938,7 @@ fn json_merge_skip_null(base: &mut serde_json::Value, patch: &serde_json::Value) if pv.is_null() { continue; } - json_merge_skip_null( - b.entry(k.clone()).or_insert(serde_json::Value::Null), - pv, - ); + json_merge_skip_null(b.entry(k.clone()).or_insert(serde_json::Value::Null), pv); } } (b, p) => *b = p.clone(), @@ -1076,7 +1078,11 @@ pub async fn admin_asset(Path(path): Path) -> Response { } else { "application/octet-stream" }; - ([(axum::http::header::CONTENT_TYPE, mime)], f.data.into_owned()).into_response() + ( + [(axum::http::header::CONTENT_TYPE, mime)], + f.data.into_owned(), + ) + .into_response() } None => StatusCode::NOT_FOUND.into_response(), } @@ -1097,7 +1103,10 @@ pub fn routes() -> Router> { .route("/admin/keys/:id", delete(admin_keys_revoke)) .route("/admin/keys/:id/rotate", post(admin_keys_rotate)) .route("/admin/config", get(admin_config_get).put(admin_config_put)) - .route("/admin/invites", post(admin_invites_mint).get(admin_invites_list)) + .route( + "/admin/invites", + post(admin_invites_mint).get(admin_invites_list), + ) } #[cfg(test)] @@ -1111,9 +1120,15 @@ mod tests { fn admin_console_is_embedded() { let index = AdminAssets::get("index.html").expect("admin-ui/dist/index.html embedded"); let html = std::str::from_utf8(&index.data).expect("index.html is utf-8"); - assert!(html.contains("Smirk Operator Console"), "console title missing"); + assert!( + html.contains("Smirk Operator Console"), + "console title missing" + ); // Vite rewrites asset URLs to the /admin/ base; the served route matches. - assert!(html.contains("/admin/assets/"), "asset base not rewritten to /admin/"); + assert!( + html.contains("/admin/assets/"), + "asset base not rewritten to /admin/" + ); assert!( AdminAssets::iter().any(|p| p.starts_with("assets/")), "no built JS/CSS assets embedded", @@ -1145,8 +1160,12 @@ mod tests { .unwrap(); assert_eq!(res.status(), axum::http::StatusCode::OK); assert_eq!(res.headers()["content-type"], "text/html; charset=utf-8"); - let body = axum::body::to_bytes(res.into_body(), 1 << 20).await.unwrap(); - assert!(std::str::from_utf8(&body).unwrap().contains("Smirk Operator Console")); + let body = axum::body::to_bytes(res.into_body(), 1 << 20) + .await + .unwrap(); + assert!(std::str::from_utf8(&body) + .unwrap() + .contains("Smirk Operator Console")); // GET /admin/assets/ -> served with a sane content-type. let asset = AdminAssets::iter() diff --git a/src/api/auth.rs b/src/api/auth.rs index eb58b98..e441f33 100644 --- a/src/api/auth.rs +++ b/src/api/auth.rs @@ -133,9 +133,17 @@ fn parse_asset(asset: &str) -> Result { /// Parse the wire key list into the `(AssetType, public_key, public_spend_key)` /// tuples the atomic-registration DB path binds directly. A bad asset is a 400. -fn parse_keys(keys: &[AssetPublicKey]) -> Result)>, AppError> { +fn parse_keys( + keys: &[AssetPublicKey], +) -> Result)>, AppError> { keys.iter() - .map(|k| Ok((parse_asset(&k.asset)?, k.public_key.clone(), k.public_spend_key.clone()))) + .map(|k| { + Ok(( + parse_asset(&k.asset)?, + k.public_key.clone(), + k.public_spend_key.clone(), + )) + }) .collect() } @@ -178,14 +186,10 @@ fn pow_applies(state: &AppState, pubkey_hash_lc: &str) -> bool { /// request Host. Fail closed when unset (Nostr identity is disabled). fn nip98_url(state: &AppState, path: &str) -> Result { let cfg = state.cfg(); - let base = cfg - .identity - .public_api_url - .as_deref() - .ok_or_else(|| { - warn!("Nostr endpoint reached but PUBLIC_API_URL is unset; refusing"); - AppError::AuthError("Nostr authentication is not enabled".into()) - })?; + let base = cfg.identity.public_api_url.as_deref().ok_or_else(|| { + warn!("Nostr endpoint reached but PUBLIC_API_URL is unset; refusing"); + AppError::AuthError("Nostr authentication is not enabled".into()) + })?; Ok(format!("{}{}", base.trim_end_matches('/'), path)) } @@ -540,7 +544,10 @@ pub async fn extension_register( .db .create_user_consuming_gates( gates.invite_code_hash.as_deref(), - gates.payment.as_ref().map(|(id, pkh)| (id.as_str(), pkh.as_str())), + gates + .payment + .as_ref() + .map(|(id, pkh)| (id.as_str(), pkh.as_str())), NewUser { username: req.username.clone(), pubkey_hash: Some(pubkey_hash.clone()), @@ -699,9 +706,14 @@ struct GatedRegistration { /// Require an invite code and return its hash, erroring if the gate needs one but /// none was supplied (the presence check + literal the old `enforce_invite` used). fn require_invite_hash(invite_code: Option<&str>) -> Result { - let code = invite_code.map(str::trim).filter(|c| !c.is_empty()).ok_or_else(|| { - AppError::ValidationError("An invite code is required to register on this instance.".into()) - })?; + let code = invite_code + .map(str::trim) + .filter(|c| !c.is_empty()) + .ok_or_else(|| { + AppError::ValidationError( + "An invite code is required to register on this instance.".into(), + ) + })?; Ok(crate::core::invite::hash_invite_code(code)) } @@ -729,7 +741,10 @@ async fn plan_gate_consume( has_invite, has_payment, ) { - GatePlan::Open => Ok(GatedRegistration { invite_code_hash: None, payment: None }), + GatePlan::Open => Ok(GatedRegistration { + invite_code_hash: None, + payment: None, + }), GatePlan::All => { let invite_code_hash = if state.cfg().registration.require_invite { Some(require_invite_hash(invite_code)?) @@ -1711,7 +1726,10 @@ pub async fn nostr_register( .db .create_user_consuming_gates( gates.invite_code_hash.as_deref(), - gates.payment.as_ref().map(|(id, pkh)| (id.as_str(), pkh.as_str())), + gates + .payment + .as_ref() + .map(|(id, pkh)| (id.as_str(), pkh.as_str())), NewUser { username: req.username.clone(), pubkey_hash: None, @@ -1771,7 +1789,10 @@ pub fn routes() -> Router> { .route("/auth/logout", post(logout)) .route("/auth/me", get(get_me)) .route("/auth/nostr", post(nostr_login)) - .route("/auth/nostr/register-challenge", get(nostr_register_challenge)) + .route( + "/auth/nostr/register-challenge", + get(nostr_register_challenge), + ) .route("/auth/nostr/register", post(nostr_register)) .route("/auth/nostr/link-challenge", get(nostr_link_challenge)) .route("/auth/nostr/link", post(nostr_link)) @@ -1832,7 +1853,10 @@ mod tests { #[test] fn any_mode_single_gate_is_just_that_gate() { assert_eq!(plan_gates(Any, true, false, false, false), GatePlan::Invite); - assert_eq!(plan_gates(Any, false, true, false, false), GatePlan::Payment); + assert_eq!( + plan_gates(Any, false, true, false, false), + GatePlan::Payment + ); } #[test] diff --git a/src/api/landing.rs b/src/api/landing.rs index 59cdf02..22c3c85 100644 --- a/src/api/landing.rs +++ b/src/api/landing.rs @@ -104,11 +104,7 @@ pub async fn root(State(state): State>) -> Response { return StatusCode::NOT_FOUND.into_response(); } let cfg = state.cfg(); - let title = cfg - .landing - .title - .as_deref() - .unwrap_or("smirk-backend-core"); + let title = cfg.landing.title.as_deref().unwrap_or("smirk-backend-core"); let t = html_escape(title); let html = format!( "\ diff --git a/src/api/tips.rs b/src/api/tips.rs index 91cced9..0792c8b 100644 --- a/src/api/tips.rs +++ b/src/api/tips.rs @@ -321,7 +321,9 @@ pub async fn create_social_tip( let encrypted_key: Option> = match req.encrypted_key.as_deref() { Some(h) if !h.is_empty() => { if h.len() > MAX_ENCRYPTED_KEY_HEX { - return Err(AppError::ValidationError("encrypted_key is too large.".into())); + return Err(AppError::ValidationError( + "encrypted_key is too large.".into(), + )); } Some( hex::decode(h) @@ -560,10 +562,14 @@ pub async fn attach_funding( // DB length-overflow 500 (funding_txid is VARCHAR(128)). let funding_txid = req.funding_txid.trim(); if funding_txid.is_empty() { - return Err(AppError::ValidationError("funding_txid is required.".into())); + return Err(AppError::ValidationError( + "funding_txid is required.".into(), + )); } if funding_txid.len() > 128 { - return Err(AppError::ValidationError("funding_txid is too long.".into())); + return Err(AppError::ValidationError( + "funding_txid is too long.".into(), + )); } // The DB fn returns Ok(row) / NotFound / ValidationError; just surface it. @@ -716,10 +722,7 @@ pub fn routes() -> Router> { .route("/tips/social/claimable", get(get_claimable_social_tips)) .route("/tips/social/:tip_id/public", get(get_public_social_tip)) .route("/tips/social/:tip_id/cancel", post(cancel_social_tip)) - .route( - "/tips/social/:tip_id/attach-funding", - post(attach_funding), - ) + .route("/tips/social/:tip_id/attach-funding", post(attach_funding)) .route("/tips/social/:tip_id/claim", post(claim_social_tip)) .route("/tips/social/:tip_id/confirm-sweep", post(confirm_sweep)) .route("/tips/social/:tip_id/clawback", post(clawback_social_tip)) diff --git a/src/config.rs b/src/config.rs index aa6bd82..e36b485 100644 --- a/src/config.rs +++ b/src/config.rs @@ -43,7 +43,10 @@ fn looks_placeholder(s: &str) -> bool { /// never leaves the host (e.g. a payment processor reached over an SSH tunnel or /// a local reverse proxy), so the https requirement is relaxed for it. fn is_loopback_url(s: &str) -> bool { - match url::Url::parse(s).ok().and_then(|u| u.host().map(|h| h.to_owned())) { + match url::Url::parse(s) + .ok() + .and_then(|u| u.host().map(|h| h.to_owned())) + { Some(url::Host::Ipv4(ip)) => ip.is_loopback(), Some(url::Host::Ipv6(ip)) => ip.is_loopback(), Some(url::Host::Domain(d)) => d.eq_ignore_ascii_case("localhost"), @@ -1052,7 +1055,10 @@ impl Config { // booting a feed that can never populate (which would still advertise // prices:true and silently serve nothing). if self.features.prices { - if !matches!(self.features.prices_provider.as_str(), "coingecko" | "kraken") { + if !matches!( + self.features.prices_provider.as_str(), + "coingecko" | "kraken" + ) { return Err(cfg_err(format!( "PRICES_PROVIDER {:?} is not supported; supported: coingecko, kraken", self.features.prices_provider @@ -1300,7 +1306,8 @@ impl Config { if p.provider_url.trim().is_empty() { return Err(cfg_err("PREMIUM_ENABLED needs PAYMENT_PROVIDER_URL")); } - if prod && !p.provider_url.starts_with("https://") && !is_loopback_url(&p.provider_url) { + if prod && !p.provider_url.starts_with("https://") && !is_loopback_url(&p.provider_url) + { return Err(cfg_err( "PAYMENT_PROVIDER_URL must be https:// in production (loopback exempt)", )); @@ -1606,11 +1613,12 @@ mod tests { #[test] fn overlay_toggles_registration_gates() { use crate::config_overlay::SettingsOverlay; - let ov: SettingsOverlay = serde_json::from_str( - r#"{"registration":{"require_invite":true,"gate_mode":"any"}}"#, - ) - .unwrap(); - let applied = valid().apply_overlay(&ov).expect("valid registration overlay applies"); + let ov: SettingsOverlay = + serde_json::from_str(r#"{"registration":{"require_invite":true,"gate_mode":"any"}}"#) + .unwrap(); + let applied = valid() + .apply_overlay(&ov) + .expect("valid registration overlay applies"); assert!(applied.registration.require_invite); assert_eq!(applied.registration.gate_mode, GateMode::Any); } @@ -1697,7 +1705,10 @@ mod tests { c.features.tips = true; c.features.chains.btc = true; c.tip_share_base = None; - assert!(c.validate().is_err(), "tips on without a share base must fail"); + assert!( + c.validate().is_err(), + "tips on without a share base must fail" + ); } #[test] diff --git a/src/config_overlay.rs b/src/config_overlay.rs index f44d598..ef05cbf 100644 --- a/src/config_overlay.rs +++ b/src/config_overlay.rs @@ -554,10 +554,9 @@ mod tests { #[test] fn overlay_parses_sparse_json_ignoring_absent_and_unknown_sections() { // Absent sections stay None; unknown sections are ignored (forward-compat). - let ov: SettingsOverlay = serde_json::from_str( - r#"{"restore":{"policy":"unlimited"},"future_section":{"x":1}}"#, - ) - .expect("sparse overlay parses"); + let ov: SettingsOverlay = + serde_json::from_str(r#"{"restore":{"policy":"unlimited"},"future_section":{"x":1}}"#) + .expect("sparse overlay parses"); assert!(ov.landing.is_none()); assert!(ov.retention.is_none()); assert_eq!(ov.restore.unwrap().policy.as_deref(), Some("unlimited")); @@ -565,7 +564,10 @@ mod tests { #[test] fn runtime_class_classifies_known_and_defaults_restart_for_unknown() { - assert_eq!(runtime_class("landing", "enabled"), RuntimeClass::RuntimeSafe); + assert_eq!( + runtime_class("landing", "enabled"), + RuntimeClass::RuntimeSafe + ); assert_eq!( runtime_class("restore", "max_depth_days"), RuntimeClass::RuntimeSafe @@ -579,7 +581,10 @@ mod tests { runtime_class("console", "restart_apply_mode"), RuntimeClass::RuntimeSafe ); - assert_eq!(runtime_class("mystery", "field"), RuntimeClass::RestartRequired); + assert_eq!( + runtime_class("mystery", "field"), + RuntimeClass::RestartRequired + ); } #[test] @@ -598,6 +603,8 @@ mod tests { for (section, field) in EDITABLE_FIELDS { let _ = runtime_class(section, field); // no panic / all covered } - assert!(EDITABLE_FIELDS.iter().any(|(s, f)| *s == "console" && *f == "restart_apply_mode")); + assert!(EDITABLE_FIELDS + .iter() + .any(|(s, f)| *s == "console" && *f == "restart_apply_mode")); } } diff --git a/src/infra/db/operator_settings.rs b/src/infra/db/operator_settings.rs index af336c4..b537f2f 100644 --- a/src/infra/db/operator_settings.rs +++ b/src/infra/db/operator_settings.rs @@ -28,13 +28,7 @@ fn canonical_json(v: &Value) -> String { keys.sort(); let inner: Vec = keys .into_iter() - .map(|k| { - format!( - "{}:{}", - Value::String(k.clone()), - canonical_json(&map[k]) - ) - }) + .map(|k| format!("{}:{}", Value::String(k.clone()), canonical_json(&map[k]))) .collect(); format!("{{{}}}", inner.join(",")) } diff --git a/src/infra/db/social_tips.rs b/src/infra/db/social_tips.rs index 8a265e1..f3c0c17 100644 --- a/src/infra/db/social_tips.rs +++ b/src/infra/db/social_tips.rs @@ -169,11 +169,9 @@ impl Database { /// `pending_confirmation` so the funding verifier (not the caller) owns the /// transition to `pending`. #[instrument(skip(self, new))] - pub async fn create_social_tip( - &self, - new: NewSocialTip<'_>, - ) -> Result { - self.insert_social_tip(new, TipStatus::PendingConfirmation).await + pub async fn create_social_tip(&self, new: NewSocialTip<'_>) -> Result { + self.insert_social_tip(new, TipStatus::PendingConfirmation) + .await } /// All tips this user has sent, newest first (every status). @@ -858,9 +856,7 @@ impl Database { /// cleared the funding verifier and is a real tip the sender expects claimed; /// it must NOT be auto-cancelled here. #[instrument(skip(self))] - pub async fn cancel_stuck_pending_confirmation( - &self, - ) -> Result, AppError> { + pub async fn cancel_stuck_pending_confirmation(&self) -> Result, AppError> { let rows = sqlx::query_as::<_, GcCancelledTip>( "UPDATE social_tips \ SET status = 'cancelled', updated_at = NOW() \ diff --git a/src/infra/db/users.rs b/src/infra/db/users.rs index f127efa..7095d8b 100644 --- a/src/infra/db/users.rs +++ b/src/infra/db/users.rs @@ -230,10 +230,7 @@ impl Database { /// registered on *this* backend. Used to look up a recipient's linked Nostr /// pubkey so a bare-`grin1…`-addressed send can route over Nostr gift-wrap. #[instrument(skip(self, address))] - pub async fn find_user_by_grin_address( - &self, - address: &str, - ) -> Result, AppError> { + pub async fn find_user_by_grin_address(&self, address: &str) -> Result, AppError> { let sql = format!( "SELECT {USER_COLS} FROM users u \ JOIN user_keys k ON k.user_id = u.id \ diff --git a/src/infra/electrum.rs b/src/infra/electrum.rs index a5742f0..12fc03e 100644 --- a/src/infra/electrum.rs +++ b/src/infra/electrum.rs @@ -536,7 +536,8 @@ impl ElectrumClient { let count = self.primary_fail_count.fetch_add(1, Ordering::Relaxed) + 1; if count >= PRIMARY_FAIL_THRESHOLD { let until = now_ms.saturating_add(PRIMARY_COOLDOWN.as_millis() as u64); - self.primary_cooldown_until_ms.store(until, Ordering::Relaxed); + self.primary_cooldown_until_ms + .store(until, Ordering::Relaxed); } } @@ -981,7 +982,10 @@ mod tests { let client = ElectrumClient::new(UtxoNetwork::BitcoinMainnet, &cfg).unwrap(); let tip = client.get_tip_height().await; assert!(tip.is_ok(), "lenient TLS must connect to {server}: {tip:?}"); - assert!(tip.unwrap() > 800_000, "sanity: BTC tip height from {server}"); + assert!( + tip.unwrap() > 800_000, + "sanity: BTC tip height from {server}" + ); } } diff --git a/src/infra/grin/mod.rs b/src/infra/grin/mod.rs index d374646..4f28c08 100644 --- a/src/infra/grin/mod.rs +++ b/src/infra/grin/mod.rs @@ -158,9 +158,7 @@ impl GrinOutputInfo { /// bogus count. Mirrors [`ViewWalletOutputResult::confirmations`]. pub fn confirmations(&self, tip_height: u64) -> u64 { match self.block_height { - Some(h) if h > 0 && h <= tip_height => { - tip_height.saturating_sub(h).saturating_add(1) - } + Some(h) if h > 0 && h <= tip_height => tip_height.saturating_sub(h).saturating_add(1), _ => 0, } } diff --git a/src/infra/grin_lws/client.rs b/src/infra/grin_lws/client.rs index c06e6be..6dfb3c0 100644 --- a/src/infra/grin_lws/client.rs +++ b/src/infra/grin_lws/client.rs @@ -170,7 +170,10 @@ impl GrinLwsClient { } fn node_err_status(&self, label: &str, status: reqwest::StatusCode) -> AppError { - AppError::NodeError(format!("grin-lws {label} failed (HTTP {})", status.as_u16())) + AppError::NodeError(format!( + "grin-lws {label} failed (HTTP {})", + status.as_u16() + )) } } diff --git a/src/infra/lws/client.rs b/src/infra/lws/client.rs index 48d437d..6fec50f 100644 --- a/src/infra/lws/client.rs +++ b/src/infra/lws/client.rs @@ -242,8 +242,7 @@ impl LwsClient { Some(_) => Ok(()), None => { self.admin_add_account(address, view_key).await?; - let current = - self.account_scan_height(address).await?.unwrap_or(u64::MAX); + let current = self.account_scan_height(address).await?.unwrap_or(u64::MAX); if start_height < current { // The account is now added at the chain tip. If this backfill // rescan fails, the account is stranded at the tip: it reads a diff --git a/src/infra/prices.rs b/src/infra/prices.rs index 8a7c428..9835d40 100644 --- a/src/infra/prices.rs +++ b/src/infra/prices.rs @@ -27,7 +27,11 @@ const KRAKEN_URL: &str = "https://api.kraken.com/0/public/Ticker"; /// Nonlogs markets — WOW/GRIN, which Kraken doesn't list (priced via BTC/USDT). const NONLOGS_URL: &str = "https://api.nonlogs.io/api/markets"; /// Our majors → Kraken USD pair. WOW/GRIN come from nonlogs instead. -const KRAKEN_PAIRS: &[(&str, &str)] = &[("btc", "XXBTZUSD"), ("ltc", "XLTCZUSD"), ("xmr", "XXMRZUSD")]; +const KRAKEN_PAIRS: &[(&str, &str)] = &[ + ("btc", "XXBTZUSD"), + ("ltc", "XLTCZUSD"), + ("xmr", "XXMRZUSD"), +]; /// Lower bound on the refresh interval, to stay within free-tier rate limits. const MIN_INTERVAL_SECS: u64 = 60; /// Upper bound on the price response body. The real payload is a handful of @@ -146,7 +150,12 @@ impl PriceClient { .collect(); if !pairs.is_empty() { let joined = pairs.iter().map(|(_, p)| *p).collect::>().join(","); - if let Ok(resp) = self.http.get(KRAKEN_URL).query(&[("pair", joined.as_str())]).send().await + if let Ok(resp) = self + .http + .get(KRAKEN_URL) + .query(&[("pair", joined.as_str())]) + .send() + .await { if resp.status().is_success() { let body = read_capped(resp, MAX_PRICE_BODY_BYTES).await?; diff --git a/src/infra/relay/nauthz.rs b/src/infra/relay/nauthz.rs index 47ab874..ca183ee 100644 --- a/src/infra/relay/nauthz.rs +++ b/src/infra/relay/nauthz.rs @@ -91,13 +91,12 @@ impl Authorization for AdmissionService { // Premium membership — only the `premium-post` policy consults it, so skip // the (cheap, indexed) lookup entirely under the other policies (and when // the author is already write-allowlisted). - let author_premium = if !author_allowlisted - && self.relay.write_policy() == WritePolicy::PremiumPost - { - self.db.is_premium_npub(&author_hex).await.unwrap_or(false) - } else { - false - }; + let author_premium = + if !author_allowlisted && self.relay.write_policy() == WritePolicy::PremiumPost { + self.db.is_premium_npub(&author_hex).await.unwrap_or(false) + } else { + false + }; // Recipient registration (only when the author isn't already registered / // allowlisted): collect up to MAX_P_TAGS distinct `p` tags and resolve them diff --git a/src/infra/relay/policy.rs b/src/infra/relay/policy.rs index 34d6ca9..e38cb8f 100644 --- a/src/infra/relay/policy.rs +++ b/src/infra/relay/policy.rs @@ -219,8 +219,26 @@ mod tests { #[test] fn author_allowlist_gates_on_author() { let m = meta(A, 1, FF); - assert!(decide(&m, WritePolicy::AuthorAllowlist, 0, true, false, false, false).is_permit()); - assert!(!decide(&m, WritePolicy::AuthorAllowlist, 0, false, false, false, false).is_permit()); + assert!(decide( + &m, + WritePolicy::AuthorAllowlist, + 0, + true, + false, + false, + false + ) + .is_permit()); + assert!(!decide( + &m, + WritePolicy::AuthorAllowlist, + 0, + false, + false, + false, + false + ) + .is_permit()); } #[test] diff --git a/src/models/tip_status.rs b/src/models/tip_status.rs index 7b4717d..56f9887 100644 --- a/src/models/tip_status.rs +++ b/src/models/tip_status.rs @@ -126,7 +126,10 @@ mod tests { #[test] fn wire_strings_match_check_constraint() { assert_eq!(TipStatus::Draft.as_str(), "draft"); - assert_eq!(TipStatus::PendingConfirmation.as_str(), "pending_confirmation"); + assert_eq!( + TipStatus::PendingConfirmation.as_str(), + "pending_confirmation" + ); assert_eq!(TipStatus::Pending.as_str(), "pending"); assert_eq!(TipStatus::Claiming.as_str(), "claiming"); assert_eq!(TipStatus::Claimed.as_str(), "claimed"); diff --git a/src/tips/confirmation.rs b/src/tips/confirmation.rs index 2f554df..c9adf5e 100644 --- a/src/tips/confirmation.rs +++ b/src/tips/confirmation.rs @@ -74,7 +74,11 @@ async fn check_asset_confirmations(state: &AppState, asset: &str) -> Result<(), debug!(asset, "no tips awaiting confirmation"); return Ok(()); } - info!(asset, count = tips.len(), "checking tip funding confirmations"); + info!( + asset, + count = tips.len(), + "checking tip funding confirmations" + ); for tip in &tips { let funding_txid = match tip.funding_txid.as_deref() { @@ -125,7 +129,10 @@ async fn check_grin_confirmations(state: &AppState) -> Result<(), AppError> { debug!("no grin tips awaiting confirmation"); return Ok(()); } - info!(count = tips.len(), "checking grin tip funding confirmations"); + info!( + count = tips.len(), + "checking grin tip funding confirmations" + ); // One tip-height read for the whole batch. A node error here aborts the pass // (logged/swallowed by the caller) leaving every row untouched — never a @@ -427,7 +434,11 @@ async fn process_pending_verifications(state: &AppState) -> Result<(), AppError> match verify_funding_amount(state, tip).await { FundingVerification::Verified { observed } => { let observed_i64 = i64::try_from(observed).unwrap_or(i64::MAX); - match state.db.mark_tip_funding_verified(tip.id, observed_i64).await { + match state + .db + .mark_tip_funding_verified(tip.id, observed_i64) + .await + { Ok(Some(_updated)) => { info!( tip_id = %tip.id, asset = %tip.asset, @@ -437,20 +448,32 @@ async fn process_pending_verifications(state: &AppState) -> Result<(), AppError> // Public tips deliver by share URL: no recipient DM / // announcement side-effect to fire here. } - Ok(None) => debug!(tip_id = %tip.id, "verified: row no longer pending_confirmation — skipping"), - Err(e) => warn!(tip_id = %tip.id, error = %e, "mark_tip_funding_verified DB error"), + Ok(None) => { + debug!(tip_id = %tip.id, "verified: row no longer pending_confirmation — skipping") + } + Err(e) => { + warn!(tip_id = %tip.id, error = %e, "mark_tip_funding_verified DB error") + } } } FundingVerification::Short { observed } => { let observed_i64 = i64::try_from(observed).unwrap_or(i64::MAX); - match state.db.mark_tip_funding_mismatch(tip.id, observed_i64).await { + match state + .db + .mark_tip_funding_mismatch(tip.id, observed_i64) + .await + { Ok(Some(_updated)) => warn!( tip_id = %tip.id, asset = %tip.asset, declared = tip.amount, observed = observed_i64, "tip funding mismatch — sender funded LESS than declared" ), - Ok(None) => debug!(tip_id = %tip.id, "mismatch: row no longer pending_confirmation — skipping"), - Err(e) => warn!(tip_id = %tip.id, error = %e, "mark_tip_funding_mismatch DB error"), + Ok(None) => { + debug!(tip_id = %tip.id, "mismatch: row no longer pending_confirmation — skipping") + } + Err(e) => { + warn!(tip_id = %tip.id, error = %e, "mark_tip_funding_mismatch DB error") + } } } FundingVerification::Unfunded => { @@ -560,7 +583,13 @@ mod tests { &resp(vec![tx(1_000, 0, 100, false), tx(0, 1_000, 110, false)]), 1_000, ); - assert_eq!(r, FundingVerification::Drained { net: 0, total_sent: 1_000 }); + assert_eq!( + r, + FundingVerification::Drained { + net: 0, + total_sent: 1_000 + } + ); } #[test] diff --git a/src/tips/gc.rs b/src/tips/gc.rs index 8bd23cd..0f8bbc5 100644 --- a/src/tips/gc.rs +++ b/src/tips/gc.rs @@ -50,7 +50,10 @@ pub async fn run_tip_draft_gc_cycle(state: Arc) { ); } if !cancelled.is_empty() { - info!(count = cancelled.len(), "tip-draft GC: cancelled abandoned drafts"); + info!( + count = cancelled.len(), + "tip-draft GC: cancelled abandoned drafts" + ); } } Err(e) => warn!(error = %e, "tip-draft GC cycle failed"), diff --git a/src/tips/sweep_reconciler.rs b/src/tips/sweep_reconciler.rs index 9c6b5a2..97ea4e8 100644 --- a/src/tips/sweep_reconciler.rs +++ b/src/tips/sweep_reconciler.rs @@ -142,10 +142,7 @@ async fn acquire_advisory_lock( /// Release the advisory lock on the SAME connection it was acquired on, then /// drop the connection back to the pool. Best-effort — the lock auto-releases /// when the connection closes. -async fn release_advisory_lock( - mut conn: sqlx::pool::PoolConnection, - key: i64, -) { +async fn release_advisory_lock(mut conn: sqlx::pool::PoolConnection, key: i64) { if let Err(e) = sqlx::query("SELECT pg_advisory_unlock($1)") .bind(key) .execute(&mut *conn) @@ -172,7 +169,8 @@ pub async fn run_sweep_reconcile_cycle(state: Arc) { #[instrument(skip(state))] async fn run_confirm_cycle(state: &AppState) -> Result<(), AppError> { - let Some(lock_conn) = acquire_advisory_lock(state, SWEEP_CONFIRM_ADVISORY_LOCK_KEY).await? else { + let Some(lock_conn) = acquire_advisory_lock(state, SWEEP_CONFIRM_ADVISORY_LOCK_KEY).await? + else { debug!("sweep confirm cycle: advisory lock held by another instance — skipping"); return Ok(()); }; @@ -184,7 +182,10 @@ async fn run_confirm_cycle(state: &AppState) -> Result<(), AppError> { } async fn confirm_cycle_inner(state: &AppState) -> Result<(), AppError> { - let tips = state.db.get_tips_awaiting_sweep_confirmation(BATCH_SIZE).await?; + let tips = state + .db + .get_tips_awaiting_sweep_confirmation(BATCH_SIZE) + .await?; if tips.is_empty() { debug!("no tips awaiting sweep confirmation"); return Ok(()); @@ -257,7 +258,13 @@ async fn process_awaiting_tip(state: &AppState, tip: &SocialTipRow) -> Result<() let settled = if txid_matches { state .db - .confirm_sweep_onchain(tip.id, &winner.txid, winner.block_height as i32, block_hash, None) + .confirm_sweep_onchain( + tip.id, + &winner.txid, + winner.block_height as i32, + block_hash, + None, + ) .await? } else { debug!( @@ -267,7 +274,12 @@ async fn process_awaiting_tip(state: &AppState, tip: &SocialTipRow) -> Result<() ); state .db - .clear_claimed_by_on_settle(tip.id, &winner.txid, winner.block_height as i32, block_hash) + .clear_claimed_by_on_settle( + tip.id, + &winner.txid, + winner.block_height as i32, + block_hash, + ) .await? }; @@ -349,8 +361,9 @@ async fn probe_grin_spends( .as_ref() .ok_or_else(|| AppError::NodeError("grin client disabled".into()))?; - let commit = grin_commitment(tip) - .ok_or_else(|| AppError::ValidationError("grin commitment missing for sweep probe".into()))?; + let commit = grin_commitment(tip).ok_or_else(|| { + AppError::ValidationError("grin commitment missing for sweep probe".into()) + })?; let tip_height = grin.get_height().await? as i64; @@ -402,10 +415,9 @@ async fn probe_electrum_spends( } .ok_or_else(|| AppError::NodeError(format!("{asset} Electrum client disabled")))?; - let address = tip - .tip_address - .as_deref() - .ok_or_else(|| AppError::ValidationError("tip_address missing for Electrum probe".into()))?; + let address = tip.tip_address.as_deref().ok_or_else(|| { + AppError::ValidationError("tip_address missing for Electrum probe".into()) + })?; let history = electrum.get_history(address).await?; // Chain height comes from ELECTRUM, not a local node (the backend runs none). @@ -500,7 +512,10 @@ async fn run_reorg_cycle(state: &AppState) -> Result<(), AppError> { } async fn reorg_cycle_inner(state: &AppState) -> Result<(), AppError> { - let tips = state.db.get_settled_tips_for_reorg_check(BATCH_SIZE).await?; + let tips = state + .db + .get_settled_tips_for_reorg_check(BATCH_SIZE) + .await?; if tips.is_empty() { debug!("no settled tips to reorg-check"); return Ok(()); @@ -687,10 +702,22 @@ mod tests { // Funding tx: tip address RECEIVES; recv != 0 → not a spend. let tx = VerboseTransaction { txid: "fund".into(), - vin: vec![VerboseVin { txid: Some("alice_prev".into()), vout: Some(0), prevout: None }], + vin: vec![VerboseVin { + txid: Some("alice_prev".into()), + vout: Some(0), + prevout: None, + }], vout: vec![ - VerboseVout { value: 0.001, n: 0, script_pub_key: spk("ltc1qtip") }, - VerboseVout { value: 0.296, n: 1, script_pub_key: spk("ltc1qalice") }, + VerboseVout { + value: 0.001, + n: 0, + script_pub_key: spk("ltc1qtip"), + }, + VerboseVout { + value: 0.296, + n: 1, + script_pub_key: spk("ltc1qalice"), + }, ], }; assert!(!is_verbose_tx_spend_of(&tx, "ltc1qtip").unwrap()); @@ -701,8 +728,16 @@ mod tests { // Sweep: tip address absent from outputs (recv == 0) → spend. let tx = VerboseTransaction { txid: "sweep".into(), - vin: vec![VerboseVin { txid: Some("fund".into()), vout: Some(0), prevout: None }], - vout: vec![VerboseVout { value: 0.00099, n: 0, script_pub_key: spk("ltc1qbob") }], + vin: vec![VerboseVin { + txid: Some("fund".into()), + vout: Some(0), + prevout: None, + }], + vout: vec![VerboseVout { + value: 0.00099, + n: 0, + script_pub_key: spk("ltc1qbob"), + }], }; assert!(is_verbose_tx_spend_of(&tx, "ltc1qtip").unwrap()); } @@ -713,10 +748,22 @@ mod tests { // recv != 0 → not a spend (guards the funding-with-change case). let tx = VerboseTransaction { txid: "pay".into(), - vin: vec![VerboseVin { txid: Some("payer_prev".into()), vout: Some(0), prevout: None }], + vin: vec![VerboseVin { + txid: Some("payer_prev".into()), + vout: Some(0), + prevout: None, + }], vout: vec![ - VerboseVout { value: 0.5, n: 0, script_pub_key: spk("ltc1qtip") }, - VerboseVout { value: 0.3, n: 1, script_pub_key: spk("ltc1qpayer") }, + VerboseVout { + value: 0.5, + n: 0, + script_pub_key: spk("ltc1qtip"), + }, + VerboseVout { + value: 0.3, + n: 1, + script_pub_key: spk("ltc1qpayer"), + }, ], }; assert!(!is_verbose_tx_spend_of(&tx, "ltc1qtip").unwrap()); diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 01b76d0..c32cf0e 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -197,13 +197,14 @@ impl TestApp { extra_headers: &[(&str, &str)], body: Option, ) -> (StatusCode, axum::http::HeaderMap, Value) { - let mut builder = Request::builder() - .method(method) - .uri(uri) - .extension(axum::extract::ConnectInfo(std::net::SocketAddr::from(( - [127, 0, 0, 1], - 0, - )))); + let mut builder = + Request::builder() + .method(method) + .uri(uri) + .extension(axum::extract::ConnectInfo(std::net::SocketAddr::from(( + [127, 0, 0, 1], + 0, + )))); for (k, v) in extra_headers { builder = builder.header(*k, *v); } diff --git a/tests/nostr_register.rs b/tests/nostr_register.rs index 1d8d4b8..fff124c 100644 --- a/tests/nostr_register.rs +++ b/tests/nostr_register.rs @@ -138,14 +138,23 @@ async fn register_replays_are_rejected() { "keys": [{ "asset": "btc", "public_key": format!("btcpub-{}", &pk[..16]) }], }); let (s1, _) = app - .request("POST", "/api/v1/auth/nostr/register", None, Some(body.clone())) + .request( + "POST", + "/api/v1/auth/nostr/register", + None, + Some(body.clone()), + ) .await; assert_eq!(s1, StatusCode::OK); // Same nonce again -> consumed -> rejected (single-use replay guard). let (s2, _) = app .request("POST", "/api/v1/auth/nostr/register", None, Some(body)) .await; - assert_ne!(s2, StatusCode::OK, "a consumed nonce must not register again"); + assert_ne!( + s2, + StatusCode::OK, + "a consumed nonce must not register again" + ); } #[tokio::test] diff --git a/tests/registration_atomicity.rs b/tests/registration_atomicity.rs index 056dac1..b548d8f 100644 --- a/tests/registration_atomicity.rs +++ b/tests/registration_atomicity.rs @@ -35,7 +35,9 @@ async fn happy_path_consumes_tokens_creates_user_and_persists_keys() { let db = &app.state.db; let hash = hash_invite_code(&generate_invite_code()); - db.insert_invite_code(&hash, Some("atomic-test")).await.unwrap(); + db.insert_invite_code(&hash, Some("atomic-test")) + .await + .unwrap(); let pkh = format!("pay-pkh-{}", Uuid::new_v4()); let invoice = format!("inv-{}", Uuid::new_v4()); @@ -76,14 +78,19 @@ async fn username_collision_rolls_back_the_invite() { // A fresh, claimable invite. let hash = hash_invite_code(&generate_invite_code()); - db.insert_invite_code(&hash, Some("atomic-test")).await.unwrap(); + db.insert_invite_code(&hash, Some("atomic-test")) + .await + .unwrap(); // Register a NEW wallet that collides on the taken username. let err = db .create_user_consuming_gates(Some(&hash), None, new_user(Some(&taken)), &keys()) .await .expect_err("a colliding username must fail the registration"); - assert!(matches!(err, AppError::Conflict(_)), "expected 409, got {err:?}"); + assert!( + matches!(err, AppError::Conflict(_)), + "expected 409, got {err:?}" + ); // The invite must NOT have been burned — the tx rolled back. assert!( @@ -99,7 +106,9 @@ async fn failed_payment_consume_does_not_burn_the_invite() { // A fresh, claimable invite. let hash = hash_invite_code(&generate_invite_code()); - db.insert_invite_code(&hash, Some("atomic-test")).await.unwrap(); + db.insert_invite_code(&hash, Some("atomic-test")) + .await + .unwrap(); // A payment invoice that is ALREADY spent (simulates losing the consume race // in `all` mode after the invite was claimed). diff --git a/tests/tips.rs b/tests/tips.rs index a0f8e75..21e9471 100644 --- a/tests/tips.rs +++ b/tests/tips.rs @@ -42,7 +42,12 @@ async fn public_tip_draft_create_get_cancel_flow() { // 1. Create a draft (no funding_txid) -> draft + share_url. let (status, body) = app - .request("POST", "/api/v1/tips/social", Some(&token), Some(draft_body())) + .request( + "POST", + "/api/v1/tips/social", + Some(&token), + Some(draft_body()), + ) .await; assert_eq!(status, StatusCode::OK, "create: {body}"); assert_eq!(body["status"], "draft"); @@ -51,7 +56,12 @@ async fn public_tip_draft_create_get_cancel_flow() { // 2. Public read (UNAUTH) surfaces it and is not yet claimable. let (status, pub_body) = app - .request("GET", &format!("/api/v1/tips/social/{tip_id}/public"), None, None) + .request( + "GET", + &format!("/api/v1/tips/social/{tip_id}/public"), + None, + None, + ) .await; assert_eq!(status, StatusCode::OK, "get_public: {pub_body}"); assert_eq!(pub_body["is_public"], true); @@ -72,14 +82,24 @@ async fn public_tip_draft_create_get_cancel_flow() { // 4. Cancel the draft (owner) -> {ok:true}. let (status, cancel_body) = app - .request("POST", &format!("/api/v1/tips/social/{tip_id}/cancel"), Some(&token), None) + .request( + "POST", + &format!("/api/v1/tips/social/{tip_id}/cancel"), + Some(&token), + None, + ) .await; assert_eq!(status, StatusCode::OK, "cancel: {cancel_body}"); assert_eq!(cancel_body["ok"], true); // 5. Cancelling again -> 404 (no longer a draft). let (status, _) = app - .request("POST", &format!("/api/v1/tips/social/{tip_id}/cancel"), Some(&token), None) + .request( + "POST", + &format!("/api/v1/tips/social/{tip_id}/cancel"), + Some(&token), + None, + ) .await; assert_eq!(status, StatusCode::NOT_FOUND); @@ -89,7 +109,10 @@ async fn public_tip_draft_create_get_cancel_flow() { .await; assert_eq!(status, StatusCode::OK, "sent: {sent}"); let tips = sent["tips"].as_array().expect("tips array"); - let found = tips.iter().find(|t| t["id"] == tip_id).expect("tip in sent list"); + let found = tips + .iter() + .find(|t| t["id"] == tip_id) + .expect("tip in sent list"); assert_eq!(found["status"], "cancelled"); assert_eq!(found["is_public"], true); } @@ -113,7 +136,12 @@ async fn db_create_draft_roundtrips_all_columns() { grin_commitment: None, }; // Exercises the full 32-column FromRow decode on RETURNING. - let row = app.state.db.create_draft_social_tip(new).await.expect("create draft"); + let row = app + .state + .db + .create_draft_social_tip(new) + .await + .expect("create draft"); assert_eq!(row.status, "draft"); assert_eq!(row.amount, 100_000); assert!(row.is_public); @@ -136,7 +164,11 @@ async fn targeted_tip_is_rejected() { })), ) .await; - assert_eq!(status, StatusCode::BAD_REQUEST, "targeted must be rejected: {body}"); + assert_eq!( + status, + StatusCode::BAD_REQUEST, + "targeted must be rejected: {body}" + ); } #[tokio::test] @@ -147,13 +179,17 @@ async fn create_requires_positive_amount_and_claim_hash() { // Non-positive amount. let mut b = draft_body(); b["amount"] = json!(0); - let (status, _) = app.request("POST", "/api/v1/tips/social", Some(&token), Some(b)).await; + let (status, _) = app + .request("POST", "/api/v1/tips/social", Some(&token), Some(b)) + .await; assert_eq!(status, StatusCode::BAD_REQUEST); // Missing claim_key_hash on a public tip. let mut b = draft_body(); b.as_object_mut().unwrap().remove("claim_key_hash"); - let (status, _) = app.request("POST", "/api/v1/tips/social", Some(&token), Some(b)).await; + let (status, _) = app + .request("POST", "/api/v1/tips/social", Some(&token), Some(b)) + .await; assert_eq!(status, StatusCode::BAD_REQUEST); } @@ -166,7 +202,12 @@ async fn tips_disabled_returns_400() { }; let (_uid, token, _r) = app.mint_session().await; let (status, _) = app - .request("POST", "/api/v1/tips/social", Some(&token), Some(draft_body())) + .request( + "POST", + "/api/v1/tips/social", + Some(&token), + Some(draft_body()), + ) .await; assert_eq!(status, StatusCode::BAD_REQUEST, "tips off must 400"); } @@ -253,7 +294,10 @@ async fn attach_funding_conflicting_txid_is_validation_error() { .attach_funding_to_tip(tip_id, uid, "txid-DIFFERENT") .await .unwrap_err(); - assert!(matches!(err, AppError::ValidationError(_)), "expected ValidationError, got {err:?}"); + assert!( + matches!(err, AppError::ValidationError(_)), + "expected ValidationError, got {err:?}" + ); // Not-owned / unknown tip -> NotFound (404). let other = app.create_user().await; @@ -263,7 +307,10 @@ async fn attach_funding_conflicting_txid_is_validation_error() { .attach_funding_to_tip(tip_id, other, "txid-x") .await .unwrap_err(); - assert!(matches!(err, AppError::NotFound(_)), "expected NotFound, got {err:?}"); + assert!( + matches!(err, AppError::NotFound(_)), + "expected NotFound, got {err:?}" + ); } #[tokio::test] @@ -344,7 +391,10 @@ async fn mark_mismatch_transitions_to_funding_mismatch() { .mark_tip_funding_verified(tip_id, 10) .await .expect("verify after mismatch"); - assert!(verify_after.is_none(), "a funding_mismatch row must not become claimable"); + assert!( + verify_after.is_none(), + "a funding_mismatch row must not become claimable" + ); } #[tokio::test] @@ -385,7 +435,10 @@ async fn get_tips_pending_confirmation_filters() { .get_tips_pending_confirmation("xmr") .await .expect("xmr pending"); - assert!(xmr_pending.iter().any(|t| t.id == xmr_id), "funded xmr tip must be returned"); + assert!( + xmr_pending.iter().any(|t| t.id == xmr_id), + "funded xmr tip must be returned" + ); assert!( !xmr_pending.iter().any(|t| t.id == xmr_draft), "unfunded xmr draft must be excluded" @@ -477,45 +530,96 @@ async fn mark_tip_claiming_double_claim_guard() { // Fresh claimable tip: first claim locks it into 'claiming'. let id = make_pending(&app, sender, "fund-claim-1").await; - let first = app.state.db.mark_tip_claiming(id, claimant).await.expect("claim"); + let first = app + .state + .db + .mark_tip_claiming(id, claimant) + .await + .expect("claim"); assert!(first.is_some(), "first claim on a pending tip must succeed"); assert_eq!(first.unwrap().status, "claiming"); // Re-claim on a 'claiming' public tip is INTENTIONALLY allowed: the claim // state is a UX signal, not a lock — the reconciler resolves the on-chain // sweep-race winner. (Verbatim audit-T1 retry semantics.) - let reclaim = app.state.db.mark_tip_claiming(id, claimant).await.expect("reclaim"); - assert!(reclaim.is_some(), "re-claim on a claiming public tip is allowed"); + let reclaim = app + .state + .db + .mark_tip_claiming(id, claimant) + .await + .expect("reclaim"); + assert!( + reclaim.is_some(), + "re-claim on a claiming public tip is allowed" + ); // None on funding_mismatch (audit-T1 fund-loss guard). let mm = make_draft(&app, sender, "btc", 1_000_000, 0, None).await; - app.state.db.attach_funding_to_tip(mm, sender, "fund-mm").await.expect("attach"); - app.state.db.mark_tip_funding_mismatch(mm, 10).await.expect("mismatch").expect("row"); + app.state + .db + .attach_funding_to_tip(mm, sender, "fund-mm") + .await + .expect("attach"); + app.state + .db + .mark_tip_funding_mismatch(mm, 10) + .await + .expect("mismatch") + .expect("row"); assert!( - app.state.db.mark_tip_claiming(mm, claimant).await.expect("claim mm").is_none(), + app.state + .db + .mark_tip_claiming(mm, claimant) + .await + .expect("claim mm") + .is_none(), "a funding_mismatch tip must never be claimable" ); // None on an unverified (pending_confirmation) tip. let pc = make_draft(&app, sender, "btc", 100_000, 0, None).await; - app.state.db.attach_funding_to_tip(pc, sender, "fund-pc").await.expect("attach"); + app.state + .db + .attach_funding_to_tip(pc, sender, "fund-pc") + .await + .expect("attach"); assert!( - app.state.db.mark_tip_claiming(pc, claimant).await.expect("claim pc").is_none(), + app.state + .db + .mark_tip_claiming(pc, claimant) + .await + .expect("claim pc") + .is_none(), "an unverified tip must not be claimable" ); // None on a clawed_back tip. let cb = make_pending(&app, sender, "fund-cb").await; - app.state.db.clawback_social_tip(cb, sender).await.expect("clawback").expect("row"); + app.state + .db + .clawback_social_tip(cb, sender) + .await + .expect("clawback") + .expect("row"); assert!( - app.state.db.mark_tip_claiming(cb, claimant).await.expect("claim cb").is_none(), + app.state + .db + .mark_tip_claiming(cb, claimant) + .await + .expect("claim cb") + .is_none(), "a clawed_back tip must not be claimable" ); // None on an already-swept (settled) tip. let swept = make_claimed(&app, sender, claimant, "fund-swept").await; assert!( - app.state.db.mark_tip_claiming(swept, claimant).await.expect("claim swept").is_none(), + app.state + .db + .mark_tip_claiming(swept, claimant) + .await + .expect("claim swept") + .is_none(), "a settled tip must not be re-claimable" ); } @@ -531,12 +635,27 @@ async fn confirm_tip_sweep_first_recorder_wins() { let id = make_claiming(&app, sender, claimant, "fund-sweep").await; // First recorder writes txid-A; the row STAYS 'claiming' (record, not settle). - let r1 = app.state.db.confirm_tip_sweep(id, claimant, "txid-A").await.expect("record").expect("row"); + let r1 = app + .state + .db + .confirm_tip_sweep(id, claimant, "txid-A") + .await + .expect("record") + .expect("row"); assert_eq!(r1.sweep_txid.as_deref(), Some("txid-A")); - assert_eq!(r1.status, "claiming", "confirm_tip_sweep records, never settles"); + assert_eq!( + r1.status, "claiming", + "confirm_tip_sweep records, never settles" + ); // A DIFFERENT txid does not overwrite; fallthrough returns the live row (A). - let r2 = app.state.db.confirm_tip_sweep(id, claimant, "txid-B").await.expect("second").expect("live row"); + let r2 = app + .state + .db + .confirm_tip_sweep(id, claimant, "txid-B") + .await + .expect("second") + .expect("live row"); assert_eq!( r2.sweep_txid.as_deref(), Some("txid-A"), @@ -545,13 +664,24 @@ async fn confirm_tip_sweep_first_recorder_wins() { assert_eq!(r2.status, "claiming"); // Same txid again is an idempotent no-op returning A. - let r3 = app.state.db.confirm_tip_sweep(id, claimant, "txid-A").await.expect("idem").expect("row"); + let r3 = app + .state + .db + .confirm_tip_sweep(id, claimant, "txid-A") + .await + .expect("idem") + .expect("row"); assert_eq!(r3.sweep_txid.as_deref(), Some("txid-A")); // A never-claimed (pending) tip → None. let pending = make_pending(&app, sender, "fund-cs-pending").await; assert!( - app.state.db.confirm_tip_sweep(pending, claimant, "txid-x").await.expect("cs pending").is_none(), + app.state + .db + .confirm_tip_sweep(pending, claimant, "txid-x") + .await + .expect("cs pending") + .is_none(), "confirm-sweep on a non-claiming tip returns None" ); } @@ -585,7 +715,12 @@ async fn confirm_sweep_onchain_settles_claiming_only() { // Second settle is a no-op (one-way lock). assert!( - app.state.db.confirm_sweep_onchain(id, "swp-2", 501, None, None).await.expect("resettle").is_none(), + app.state + .db + .confirm_sweep_onchain(id, "swp-2", 501, None, None) + .await + .expect("resettle") + .is_none(), "an already-claimed row cannot be re-settled" ); @@ -593,11 +728,25 @@ async fn confirm_sweep_onchain_settles_claiming_only() { // entry into 'claimed'. let pending = make_pending(&app, sender, "fund-settle-pending").await; assert!( - app.state.db.confirm_sweep_onchain(pending, "swp", 1, None, None).await.expect("settle pending").is_none(), + app.state + .db + .confirm_sweep_onchain(pending, "swp", 1, None, None) + .await + .expect("settle pending") + .is_none(), "confirm_sweep_onchain requires status='claiming'" ); - let still = app.state.db.get_social_tip(pending).await.expect("get").expect("row"); - assert_eq!(still.status, "pending", "a pending tip must not be flipped to claimed"); + let still = app + .state + .db + .get_social_tip(pending) + .await + .expect("get") + .expect("row"); + assert_eq!( + still.status, "pending", + "a pending tip must not be flipped to claimed" + ); } /// (d) revert_sweep_on_reorg reverts 'claimed' -> 'claiming' ONLY, clearing the @@ -612,7 +761,13 @@ async fn revert_sweep_on_reorg_only_from_claimed() { let claimant = app.create_user().await; let id = make_claimed(&app, sender, claimant, "fund-reorg").await; - let reverted = app.state.db.revert_sweep_on_reorg(id).await.expect("revert").expect("reverted row"); + let reverted = app + .state + .db + .revert_sweep_on_reorg(id) + .await + .expect("revert") + .expect("reverted row"); assert_eq!(reverted.status, "claiming"); assert!(reverted.sweep_confirmed_at.is_none()); assert_eq!( @@ -624,14 +779,24 @@ async fn revert_sweep_on_reorg_only_from_claimed() { // Second revert is a no-op (now 'claiming', not 'claimed'). assert!( - app.state.db.revert_sweep_on_reorg(id).await.expect("re-revert").is_none(), + app.state + .db + .revert_sweep_on_reorg(id) + .await + .expect("re-revert") + .is_none(), "revert requires status='claimed'" ); // Revert on a plain pending tip → None. let pending = make_pending(&app, sender, "fund-reorg-pending").await; assert!( - app.state.db.revert_sweep_on_reorg(pending).await.expect("revert pending").is_none(), + app.state + .db + .revert_sweep_on_reorg(pending) + .await + .expect("revert pending") + .is_none(), "a pending tip cannot be reorg-reverted" ); } @@ -647,41 +812,105 @@ async fn clawback_status_matrix_and_settled_block() { // pending → clawable. let pending = make_pending(&app, sender, "cb-pending").await; - assert!(app.state.db.clawback_social_tip(pending, sender).await.expect("cb").is_some()); + assert!(app + .state + .db + .clawback_social_tip(pending, sender) + .await + .expect("cb") + .is_some()); // pending_confirmation → clawable. let pc = make_draft(&app, sender, "btc", 100_000, 0, None).await; - app.state.db.attach_funding_to_tip(pc, sender, "cb-pc").await.expect("attach"); - assert!(app.state.db.clawback_social_tip(pc, sender).await.expect("cb").is_some()); + app.state + .db + .attach_funding_to_tip(pc, sender, "cb-pc") + .await + .expect("attach"); + assert!(app + .state + .db + .clawback_social_tip(pc, sender) + .await + .expect("cb") + .is_some()); // claiming → clawable (sender racing a claimer). let claiming = make_claiming(&app, sender, claimant, "cb-claiming").await; - assert!(app.state.db.clawback_social_tip(claiming, sender).await.expect("cb").is_some()); + assert!(app + .state + .db + .clawback_social_tip(claiming, sender) + .await + .expect("cb") + .is_some()); // funding_mismatch → clawable. let mm = make_draft(&app, sender, "btc", 1_000_000, 0, None).await; - app.state.db.attach_funding_to_tip(mm, sender, "cb-mm").await.expect("attach"); - app.state.db.mark_tip_funding_mismatch(mm, 10).await.expect("mismatch").expect("row"); - assert!(app.state.db.clawback_social_tip(mm, sender).await.expect("cb").is_some()); + app.state + .db + .attach_funding_to_tip(mm, sender, "cb-mm") + .await + .expect("attach"); + app.state + .db + .mark_tip_funding_mismatch(mm, 10) + .await + .expect("mismatch") + .expect("row"); + assert!(app + .state + .db + .clawback_social_tip(mm, sender) + .await + .expect("cb") + .is_some()); // cancelled (GC'd but still-funded draft) → clawable. let cancelled = make_draft(&app, sender, "btc", 100_000, 0, None).await; - app.state.db.cancel_draft_social_tip(cancelled, sender).await.expect("cancel").expect("id"); - assert!(app.state.db.clawback_social_tip(cancelled, sender).await.expect("cb").is_some()); + app.state + .db + .cancel_draft_social_tip(cancelled, sender) + .await + .expect("cancel") + .expect("id"); + assert!(app + .state + .db + .clawback_social_tip(cancelled, sender) + .await + .expect("cb") + .is_some()); // Owner-scoped: a stranger cannot claw back; the real sender can. let owned = make_pending(&app, sender, "cb-owner").await; let stranger = app.create_user().await; assert!( - app.state.db.clawback_social_tip(owned, stranger).await.expect("cb stranger").is_none(), + app.state + .db + .clawback_social_tip(owned, stranger) + .await + .expect("cb stranger") + .is_none(), "only the sender can claw back" ); - assert!(app.state.db.clawback_social_tip(owned, sender).await.expect("cb owner").is_some()); + assert!(app + .state + .db + .clawback_social_tip(owned, sender) + .await + .expect("cb owner") + .is_some()); // BLOCKED once the sweep confirmed (sweep_confirmed_at IS NOT NULL). let swept = make_claimed(&app, sender, claimant, "cb-swept").await; assert!( - app.state.db.clawback_social_tip(swept, sender).await.expect("cb swept").is_none(), + app.state + .db + .clawback_social_tip(swept, sender) + .await + .expect("cb swept") + .is_none(), "a settled tip must not be clawed back" ); } @@ -698,14 +927,39 @@ async fn reconciler_poll_queries_select_the_right_rows() { let pending = make_pending(&app, sender, "poll-pending").await; let claimed = make_claimed(&app, sender, claimant, "poll-claimed").await; - let awaiting = app.state.db.get_tips_awaiting_sweep_confirmation(1_000_000).await.expect("awaiting"); - assert!(awaiting.iter().any(|t| t.id == claiming), "a claiming tip awaits sweep confirmation"); - assert!(!awaiting.iter().any(|t| t.id == pending), "a pending tip does not await sweep confirmation"); - assert!(!awaiting.iter().any(|t| t.id == claimed), "a settled tip does not await sweep confirmation"); + let awaiting = app + .state + .db + .get_tips_awaiting_sweep_confirmation(1_000_000) + .await + .expect("awaiting"); + assert!( + awaiting.iter().any(|t| t.id == claiming), + "a claiming tip awaits sweep confirmation" + ); + assert!( + !awaiting.iter().any(|t| t.id == pending), + "a pending tip does not await sweep confirmation" + ); + assert!( + !awaiting.iter().any(|t| t.id == claimed), + "a settled tip does not await sweep confirmation" + ); - let reorg = app.state.db.get_settled_tips_for_reorg_check(1_000_000).await.expect("reorg"); - assert!(reorg.iter().any(|t| t.id == claimed), "a settled tip with a height witness is reorg-checked"); - assert!(!reorg.iter().any(|t| t.id == claiming), "a claiming tip is not reorg-checked"); + let reorg = app + .state + .db + .get_settled_tips_for_reorg_check(1_000_000) + .await + .expect("reorg"); + assert!( + reorg.iter().any(|t| t.id == claimed), + "a settled tip with a height witness is reorg-checked" + ); + assert!( + !reorg.iter().any(|t| t.id == claiming), + "a claiming tip is not reorg-checked" + ); } // ── Stage 5: lifecycle + draft garbage-collection DB tests ──────────────────── @@ -737,13 +991,36 @@ async fn cancel_old_drafts_flips_stale_draft_only() { backdate_created_at(&app, stale, "8 days").await; let fresh = make_draft(&app, uid, "btc", 100_000, 0, None).await; - let cancelled = app.state.db.cancel_old_drafts().await.expect("cancel_old_drafts"); - assert!(cancelled.iter().any(|r| r.id == stale), "a > 7d draft must be cancelled"); - assert!(!cancelled.iter().any(|r| r.id == fresh), "a fresh draft must NOT be cancelled"); + let cancelled = app + .state + .db + .cancel_old_drafts() + .await + .expect("cancel_old_drafts"); + assert!( + cancelled.iter().any(|r| r.id == stale), + "a > 7d draft must be cancelled" + ); + assert!( + !cancelled.iter().any(|r| r.id == fresh), + "a fresh draft must NOT be cancelled" + ); - let stale_row = app.state.db.get_social_tip(stale).await.expect("get").expect("row"); + let stale_row = app + .state + .db + .get_social_tip(stale) + .await + .expect("get") + .expect("row"); assert_eq!(stale_row.status, "cancelled"); - let fresh_row = app.state.db.get_social_tip(fresh).await.expect("get").expect("row"); + let fresh_row = app + .state + .db + .get_social_tip(fresh) + .await + .expect("get") + .expect("row"); assert_eq!(fresh_row.status, "draft", "a fresh draft stays draft"); } @@ -755,11 +1032,19 @@ async fn cancel_stuck_pending_confirmation_flips_stale_only() { let uid = app.create_user().await; let stale = make_draft(&app, uid, "btc", 100_000, 0, None).await; - app.state.db.attach_funding_to_tip(stale, uid, "gc-stale-pc").await.expect("attach"); + app.state + .db + .attach_funding_to_tip(stale, uid, "gc-stale-pc") + .await + .expect("attach"); backdate_created_at(&app, stale, "8 days").await; let fresh = make_draft(&app, uid, "btc", 100_000, 0, None).await; - app.state.db.attach_funding_to_tip(fresh, uid, "gc-fresh-pc").await.expect("attach"); + app.state + .db + .attach_funding_to_tip(fresh, uid, "gc-fresh-pc") + .await + .expect("attach"); let cancelled = app .state @@ -776,9 +1061,21 @@ async fn cancel_stuck_pending_confirmation_flips_stale_only() { "a fresh pending_confirmation must NOT be cancelled" ); - let stale_row = app.state.db.get_social_tip(stale).await.expect("get").expect("row"); + let stale_row = app + .state + .db + .get_social_tip(stale) + .await + .expect("get") + .expect("row"); assert_eq!(stale_row.status, "cancelled"); - let fresh_row = app.state.db.get_social_tip(fresh).await.expect("get").expect("row"); + let fresh_row = app + .state + .db + .get_social_tip(fresh) + .await + .expect("get") + .expect("row"); assert_eq!(fresh_row.status, "pending_confirmation"); } @@ -798,14 +1095,37 @@ async fn log_stuck_claiming_returns_backdated_and_mutates_nothing() { .expect("backdate claimed_at"); let fresh = make_claiming(&app, sender, claimant, "gc-fresh-claiming").await; - let stuck = app.state.db.log_stuck_claiming().await.expect("log_stuck_claiming"); - assert!(stuck.iter().any(|r| r.id == stale), "a > 15m claiming row must be surfaced"); - assert!(!stuck.iter().any(|r| r.id == fresh), "a fresh claiming row must NOT be surfaced"); + let stuck = app + .state + .db + .log_stuck_claiming() + .await + .expect("log_stuck_claiming"); + assert!( + stuck.iter().any(|r| r.id == stale), + "a > 15m claiming row must be surfaced" + ); + assert!( + !stuck.iter().any(|r| r.id == fresh), + "a fresh claiming row must NOT be surfaced" + ); // READ-ONLY: the surfaced row is untouched — still 'claiming', no sweep confirm. - let row = app.state.db.get_social_tip(stale).await.expect("get").expect("row"); - assert_eq!(row.status, "claiming", "log_stuck_claiming must not mutate status"); - assert!(row.sweep_confirmed_at.is_none(), "log_stuck_claiming must not settle the sweep"); + let row = app + .state + .db + .get_social_tip(stale) + .await + .expect("get") + .expect("row"); + assert_eq!( + row.status, "claiming", + "log_stuck_claiming must not mutate status" + ); + assert!( + row.sweep_confirmed_at.is_none(), + "log_stuck_claiming must not settle the sweep" + ); } // ── Grin (voucher asset) tests ──────────────────────────────────────────────── @@ -865,7 +1185,13 @@ async fn grin_public_tip_create_returns_draft_and_share_url() { // Migration applied: the grin_commitment column round-trips, tip_address // mirrors it, and the grin funding threshold is 10. let uuid = tip_id.parse::().unwrap(); - let row = app.state.db.get_social_tip(uuid).await.expect("get").expect("row"); + let row = app + .state + .db + .get_social_tip(uuid) + .await + .expect("get") + .expect("row"); assert_eq!(row.asset, "grin"); assert_eq!(row.grin_commitment.as_deref(), Some(commit.as_str())); assert_eq!(row.tip_address.as_deref(), Some(commit.as_str())); @@ -874,7 +1200,9 @@ async fn grin_public_tip_create_returns_draft_and_share_url() { #[tokio::test] async fn grin_create_falls_back_to_tip_address_for_commitment() { - let Some(app) = grin_tips_app().await else { return }; + let Some(app) = grin_tips_app().await else { + return; + }; let (_uid, token, _refresh) = app.mint_session().await; let commit = grin_commit(); @@ -890,22 +1218,38 @@ async fn grin_create_falls_back_to_tip_address_for_commitment() { let (status, resp) = app .request("POST", "/api/v1/tips/social", Some(&token), Some(body)) .await; - assert_eq!(status, StatusCode::OK, "create grin (no commit field): {resp}"); - let uuid = resp["tip_id"].as_str().unwrap().parse::().unwrap(); - let row = app.state.db.get_social_tip(uuid).await.expect("get").expect("row"); + assert_eq!( + status, + StatusCode::OK, + "create grin (no commit field): {resp}" + ); + let uuid = resp["tip_id"] + .as_str() + .unwrap() + .parse::() + .unwrap(); + let row = app + .state + .db + .get_social_tip(uuid) + .await + .expect("get") + .expect("row"); assert_eq!(row.grin_commitment.as_deref(), Some(commit.as_str())); } #[tokio::test] async fn grin_create_rejects_junk_commitment() { - let Some(app) = grin_tips_app().await else { return }; + let Some(app) = grin_tips_app().await else { + return; + }; let (_uid, token, _refresh) = app.mint_session().await; // tip_address that isn't a 66-hex commitment -> clean 400. for bad in [ - "deadbeef".to_string(), // too short - "z".repeat(66), // right length, not hex - format!("09{}", "a1".repeat(33)), // 68 chars, too long + "deadbeef".to_string(), // too short + "z".repeat(66), // right length, not hex + format!("09{}", "a1".repeat(33)), // 68 chars, too long ] { let body = json!({ "asset": "grin", @@ -917,7 +1261,11 @@ async fn grin_create_rejects_junk_commitment() { let (status, resp) = app .request("POST", "/api/v1/tips/social", Some(&token), Some(body)) .await; - assert_eq!(status, StatusCode::BAD_REQUEST, "junk grin address must 400: {resp}"); + assert_eq!( + status, + StatusCode::BAD_REQUEST, + "junk grin address must 400: {resp}" + ); } // A valid tip_address but a junk explicit grin_commitment is also rejected @@ -933,7 +1281,11 @@ async fn grin_create_rejects_junk_commitment() { let (status, resp) = app .request("POST", "/api/v1/tips/social", Some(&token), Some(body)) .await; - assert_eq!(status, StatusCode::BAD_REQUEST, "junk grin_commitment must 400: {resp}"); + assert_eq!( + status, + StatusCode::BAD_REQUEST, + "junk grin_commitment must 400: {resp}" + ); } #[tokio::test] @@ -945,7 +1297,8 @@ async fn grin_create_rejected_when_chain_disabled() { c.features.chains.grin = false; c.tip_share_base = Some("https://tips.example".into()); }) - .await else { + .await + else { return; }; let (_uid, token, _refresh) = app.mint_session().await; @@ -961,7 +1314,11 @@ async fn grin_create_rejected_when_chain_disabled() { let (status, resp) = app .request("POST", "/api/v1/tips/social", Some(&token), Some(body)) .await; - assert_eq!(status, StatusCode::BAD_REQUEST, "grin disabled must 400: {resp}"); + assert_eq!( + status, + StatusCode::BAD_REQUEST, + "grin disabled must 400: {resp}" + ); } /// The grin voucher flows through the SAME draft -> attach-funding -> claiming @@ -971,7 +1328,9 @@ async fn grin_create_rejected_when_chain_disabled() { /// the grin worker arms depend on. #[tokio::test] async fn grin_tip_db_lifecycle_columns() { - let Some(app) = grin_tips_app().await else { return }; + let Some(app) = grin_tips_app().await else { + return; + }; let uid = app.create_user().await; let commit = grin_commit(); @@ -988,7 +1347,13 @@ async fn grin_tip_db_lifecycle_columns() { confirmations_required: 10, grin_commitment: Some(&commit), }; - let tip_id = app.state.db.create_draft_social_tip(new).await.expect("create draft").id; + let tip_id = app + .state + .db + .create_draft_social_tip(new) + .await + .expect("create draft") + .id; // attach-funding with the client's bookkeeping id (grin has no real txid). let r = app @@ -1001,7 +1366,11 @@ async fn grin_tip_db_lifecycle_columns() { assert_eq!(r.grin_commitment.as_deref(), Some(commit.as_str())); // Simulate the funding worker: 10 confs reached + amount auto-verified. - app.state.db.update_tip_confirmations(tip_id, 10).await.expect("confs"); + app.state + .db + .update_tip_confirmations(tip_id, 10) + .await + .expect("confs"); let verified = app .state .db @@ -1014,7 +1383,12 @@ async fn grin_tip_db_lifecycle_columns() { // Claim, then record the sweep's kernel excess as sweep_txid. let claimant = app.create_user().await; - app.state.db.mark_tip_claiming(tip_id, claimant).await.expect("claim").expect("row"); + app.state + .db + .mark_tip_claiming(tip_id, claimant) + .await + .expect("claim") + .expect("row"); let excess = format!("08{}", "bc".repeat(32)); // kernel excess (66 hex) let swept = app .state