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.
This commit is contained in:
jwinterm
2026-07-19 03:44:47 -04:00
parent 13b2557b9b
commit 586d29efe7
24 changed files with 785 additions and 233 deletions
+33 -14
View File
@@ -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<String> = 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<String>) -> 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<Arc<AppState>> {
.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/<real built asset> -> served with a sane content-type.
let asset = AdminAssets::iter()
+42 -18
View File
@@ -133,9 +133,17 @@ fn parse_asset(asset: &str) -> Result<AssetType, AppError> {
/// 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<Vec<(AssetType, String, Option<String>)>, AppError> {
fn parse_keys(
keys: &[AssetPublicKey],
) -> Result<Vec<(AssetType, String, Option<String>)>, 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<String, AppError> {
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<String, AppError> {
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<Arc<AppState>> {
.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]
+1 -5
View File
@@ -104,11 +104,7 @@ pub async fn root(State(state): State<Arc<AppState>>) -> 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!(
"<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\">\
+10 -7
View File
@@ -321,7 +321,9 @@ pub async fn create_social_tip(
let encrypted_key: Option<Vec<u8>> = 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<Arc<AppState>> {
.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))
+20 -9
View File
@@ -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]
+14 -7
View File
@@ -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"));
}
}
+1 -7
View File
@@ -28,13 +28,7 @@ fn canonical_json(v: &Value) -> String {
keys.sort();
let inner: Vec<String> = 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(","))
}
+4 -8
View File
@@ -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<SocialTipRow, AppError> {
self.insert_social_tip(new, TipStatus::PendingConfirmation).await
pub async fn create_social_tip(&self, new: NewSocialTip<'_>) -> Result<SocialTipRow, AppError> {
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<Vec<GcCancelledTip>, AppError> {
pub async fn cancel_stuck_pending_confirmation(&self) -> Result<Vec<GcCancelledTip>, AppError> {
let rows = sqlx::query_as::<_, GcCancelledTip>(
"UPDATE social_tips \
SET status = 'cancelled', updated_at = NOW() \
+1 -4
View File
@@ -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<Option<User>, AppError> {
pub async fn find_user_by_grin_address(&self, address: &str) -> Result<Option<User>, AppError> {
let sql = format!(
"SELECT {USER_COLS} FROM users u \
JOIN user_keys k ON k.user_id = u.id \
+6 -2
View File
@@ -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}"
);
}
}
+1 -3
View File
@@ -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,
}
}
+4 -1
View File
@@ -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()
))
}
}
+1 -2
View File
@@ -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
+11 -2
View File
@@ -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::<Vec<_>>().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?;
+6 -7
View File
@@ -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
+20 -2
View File
@@ -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]
+4 -1
View File
@@ -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");
+38 -9
View File
@@ -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]
+4 -1
View File
@@ -50,7 +50,10 @@ pub async fn run_tip_draft_gc_cycle(state: Arc<AppState>) {
);
}
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"),
+70 -23
View File
@@ -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<sqlx::Postgres>,
key: i64,
) {
async fn release_advisory_lock(mut conn: sqlx::pool::PoolConnection<sqlx::Postgres>, 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<AppState>) {
#[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());