style: apply cargo fmt so the release gate can run

The release gate's first real stage is `cargo fmt --check`, and it had
44 diffs across 15 files — enough to abort `create-release.sh` at step 0
before it touched a version number. Some of that drift is mine from the
last two days, some predates it in files I never opened
(bootstrap.rs, ghost_reaper.rs, openwrt/router.rs), and one is the
regenerated fips/app_ports.rs.

No behaviour change — rustfmt only.

Gate now: 8 of 9 green. The remaining red is cargo-test-weekly exiting
124, which is the 25-minute `timeout` expiring during a cold
CARGO_INCREMENTAL=0 rebuild on a loaded node — the tests never started.
Not a test failure.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-08-19 12:38:41 -04:00
co-authored by Claude Opus 5
parent bd299318c0
commit c788dff42d
15 changed files with 164 additions and 110 deletions
+14 -6
View File
@@ -105,10 +105,7 @@ async fn run_keeper(mut rx: mpsc::Receiver<String>, connected: Arc<AtomicBool>)
/// Discard queued input for `d` — used while no kiosk session exists so the
/// bounded channel can't fill with stale events.
async fn drain_for(rx: &mut mpsc::Receiver<String>, d: Duration) {
let _ = tokio::time::timeout(d, async {
while rx.recv().await.is_some() {}
})
.await;
let _ = tokio::time::timeout(d, async { while rx.recv().await.is_some() {} }).await;
}
/// Find the kiosk page target's WebSocket debugger URL. Prefers the page on
@@ -219,7 +216,11 @@ fn translate(raw: &str, cursor: &mut Cursor, id: &mut impl FnMut() -> u64) -> Ve
vec![mouse_event(id(), "mouseMoved", cursor, "none", 0, 1)]
}
Some("c") => {
let b = msg.get("b").and_then(Value::as_u64).unwrap_or(1).clamp(1, 3);
let b = msg
.get("b")
.and_then(Value::as_u64)
.unwrap_or(1)
.clamp(1, 3);
let (button, buttons) = match b {
2 => ("middle", 4),
3 => ("right", 2),
@@ -255,7 +256,14 @@ fn translate(raw: &str, cursor: &mut Cursor, id: &mut impl FnMut() -> u64) -> Ve
}
}
fn mouse_event(id: u64, kind: &str, cursor: &Cursor, button: &str, buttons: u32, clicks: u32) -> Value {
fn mouse_event(
id: u64,
kind: &str,
cursor: &Cursor,
button: &str,
buttons: u32,
clicks: u32,
) -> Value {
json!({
"id": id,
"method": "Input.dispatchMouseEvent",
+5 -1
View File
@@ -667,7 +667,11 @@ impl RpcHandler {
.get("state")
.and_then(|v| v.as_str())
.map(|s| s == "SETTLED")
.unwrap_or_else(|| body.get("settled").and_then(|v| v.as_bool()).unwrap_or(false));
.unwrap_or_else(|| {
body.get("settled")
.and_then(|v| v.as_bool())
.unwrap_or(false)
});
let amt_paid_sat = body
.get("amt_paid_sat")
.and_then(|v| v.as_str())
+6 -2
View File
@@ -43,8 +43,12 @@ impl RpcHandler {
// plus a blocking SSH verify per candidate. Inline, one click of
// "scan for routers" held a tokio worker for that whole time.
let routers = tokio::task::spawn_blocking(move || {
tokio::runtime::Handle::current()
.block_on(detect::scan_subnet(subnet, prefix, &ssh_user, &ssh_password))
tokio::runtime::Handle::current().block_on(detect::scan_subnet(
subnet,
prefix,
&ssh_user,
&ssh_password,
))
})
.await
.context("openwrt scan task")?;
+1 -3
View File
@@ -380,9 +380,7 @@ impl RpcHandler {
.and_then(|v| v.as_bool())
.unwrap_or(false);
let mut password = self
.verify_reveal_auth(&params, "the ecash seed")
.await?;
let mut password = self.verify_reveal_auth(&params, "the ecash seed").await?;
password.zeroize();
let seed =
+32 -10
View File
@@ -184,14 +184,17 @@ pub async fn ensure_doctor_installed() {
Err(e) => warn!("nginx listener repair failed (non-fatal): {:#}", e),
}
match run_ha_rpc_proxy_bind_repair().await {
Ok(true) => info!(
"HA bitcoind RPC forwarder rebound dynamically — survives network moves now"
),
Ok(true) => {
info!("HA bitcoind RPC forwarder rebound dynamically — survives network moves now")
}
Ok(false) => debug!("HA bitcoind RPC forwarder absent or already dynamic"),
Err(e) => warn!("HA RPC forwarder bind repair failed (non-fatal): {:#}", e),
}
match run_pull_never_image_repair().await {
Ok(n) if n > 0 => info!(retagged = n, "Healed quadlet image refs orphaned by registry rename"),
Ok(n) if n > 0 => info!(
retagged = n,
"Healed quadlet image refs orphaned by registry rename"
),
Ok(_) => debug!("All quadlet image refs resolve locally"),
Err(e) => warn!("Quadlet image ref repair failed (non-fatal): {:#}", e),
}
@@ -704,7 +707,12 @@ fn parse_socat_static_bind(exec_line: &str) -> Option<(String, String)> {
}
// Only rewrite units pinned to a concrete address; a unit already using
// a computed bind (or none) needs no heal.
let bind = after_listen.split("bind=").nth(1)?.split(',').next()?.trim();
let bind = after_listen
.split("bind=")
.nth(1)?
.split(',')
.next()?
.trim();
if !bind.chars().all(|c| c.is_ascii_digit() || c == '.') || bind.starts_with("127.") {
return None;
}
@@ -731,7 +739,10 @@ async fn run_ha_rpc_proxy_bind_repair() -> Result<bool> {
Ok(s) => s,
Err(_) => return Ok(false), // node never grew the forwarder
};
let Some(exec_line) = unit.lines().find(|l| l.trim_start().starts_with("ExecStart=")) else {
let Some(exec_line) = unit
.lines()
.find(|l| l.trim_start().starts_with("ExecStart="))
else {
return Ok(false);
};
let Some((port, target)) = parse_socat_static_bind(exec_line) else {
@@ -833,7 +844,11 @@ async fn run_pull_never_image_repair() -> Result<usize> {
}
async fn podman_stdout(args: &[&str]) -> String {
match tokio::process::Command::new("podman").args(args).output().await {
match tokio::process::Command::new("podman")
.args(args)
.output()
.await
{
Ok(out) if out.status.success() => String::from_utf8_lossy(&out.stdout).into_owned(),
_ => String::new(),
}
@@ -859,7 +874,8 @@ const NGINX_SITES: [&str; 2] = [
"/etc/nginx/sites-available/archipelago-http",
"/etc/nginx/sites-available/archipelago",
];
const NGINX_RESTART_DROPIN: &str = "/etc/systemd/system/nginx.service.d/10-archipelago-restart.conf";
const NGINX_RESTART_DROPIN: &str =
"/etc/systemd/system/nginx.service.d/10-archipelago-restart.conf";
/// Global IPv4 addresses on this host, minus Tailscale CGNAT (100.64/10) —
/// the same exclusion `setup-node-ca.sh` applies, for the same reason.
@@ -964,7 +980,10 @@ async fn run_nginx_listener_repair() -> Result<bool> {
let status = host_sudo(&["sh", "-lc", &script]).await?;
match status.code() {
Some(0) => changed = true,
Some(3) => warn!(site, "nginx listener repair failed its config test — rolled back"),
Some(3) => warn!(
site,
"nginx listener repair failed its config test — rolled back"
),
_ => warn!(site, "nginx listener repair helper failed"),
}
}
@@ -1826,7 +1845,10 @@ mod tests {
let healed = retarget_https_listeners(cfg, &present).expect("must heal");
assert!(healed.contains("listen 192.168.1.50:443 ssl;"));
assert!(healed.contains("listen 10.44.0.1:443 ssl;"));
assert!(!healed.contains("192.168.63.240"), "stale listener must be dropped");
assert!(
!healed.contains("192.168.63.240"),
"stale listener must be dropped"
);
// Untouched lines survive, and the repair is idempotent.
assert!(healed.contains("listen 80 default_server;"));
assert!(healed.contains("ssl_certificate /x;"));
+16 -7
View File
@@ -216,14 +216,20 @@ pub async fn reap_for_app(app_id: &str) -> usize {
let app_id = app_id.to_string();
reap_matching(move |g| {
g.name.as_deref().is_some_and(|n| {
n == app_id || n.starts_with(&format!("{app_id}-")) || n.ends_with(&format!("-{app_id}"))
n == app_id
|| n.starts_with(&format!("{app_id}-"))
|| n.ends_with(&format!("-{app_id}"))
})
})
.await
}
async fn reap_matching(pred: impl Fn(&Ghost) -> bool) -> usize {
let ghosts: Vec<Ghost> = find_ghosts().await.into_iter().filter(|g| pred(g)).collect();
let ghosts: Vec<Ghost> = find_ghosts()
.await
.into_iter()
.filter(|g| pred(g))
.collect();
if ghosts.is_empty() {
return 0;
}
@@ -237,7 +243,10 @@ async fn reap_matching(pred: impl Fn(&Ghost) -> bool) -> usize {
);
kill_ghost(ghost).await;
}
info!(count = ghosts.len(), "ghost reaper: reaped ghost containers");
info!(
count = ghosts.len(),
"ghost reaper: reaped ghost containers"
);
ghosts.len()
}
@@ -280,7 +289,9 @@ mod tests {
#[test]
fn a_truncated_or_missing_id_is_not_reapable() {
assert!(parse_conmon(&argv(&["/usr/bin/conmon", "-c", "8ea2fc65", "-n", "gitea"])).is_none());
assert!(
parse_conmon(&argv(&["/usr/bin/conmon", "-c", "8ea2fc65", "-n", "gitea"])).is_none()
);
assert!(parse_conmon(&argv(&["/usr/bin/conmon", "--api-version", "1"])).is_none());
}
@@ -295,9 +306,7 @@ mod tests {
let app = app.to_string();
let gh = g(name);
gh.name.as_deref().is_some_and(|n| {
n == app
|| n.starts_with(&format!("{app}-"))
|| n.ends_with(&format!("-{app}"))
n == app || n.starts_with(&format!("{app}-")) || n.ends_with(&format!("-{app}"))
})
};
assert!(matches("gitea", "gitea"));
+3 -37
View File
@@ -6,41 +6,7 @@
//! no listener, so allowing them is inert.
pub const APP_LAUNCH_PORTS: &[u16] = &[
2283,
2342,
3000,
3001,
3002,
4080,
5180,
7778,
8080,
8081,
8082,
8083,
8084,
8085,
8087,
8088,
8089,
8090,
8096,
8123,
8175,
8176,
8187,
8240,
8334,
8336,
8888,
8999,
9000,
9100,
10380,
11434,
18081,
18083,
23000,
32838,
50002,
2283, 2342, 3000, 3001, 3002, 4080, 5180, 7778, 8080, 8081, 8082, 8083, 8084, 8085, 8087, 8088,
8089, 8090, 8096, 8123, 8175, 8176, 8187, 8240, 8334, 8336, 8888, 8999, 9000, 9100, 10380,
11434, 18081, 18083, 23000, 32838, 50002,
];
+6 -2
View File
@@ -538,8 +538,12 @@ async fn same_serial_device(a: &str, b: &str) -> bool {
if a == b {
return true;
}
let ra = fs::canonicalize(a).await.unwrap_or_else(|_| PathBuf::from(a));
let rb = fs::canonicalize(b).await.unwrap_or_else(|_| PathBuf::from(b));
let ra = fs::canonicalize(a)
.await
.unwrap_or_else(|_| PathBuf::from(a));
let rb = fs::canonicalize(b)
.await
.unwrap_or_else(|_| PathBuf::from(b));
ra == rb
}
+4 -1
View File
@@ -697,7 +697,10 @@ mod tests {
// nodes would derive each other's coins.
let (other_words, _) = MasterSeed::generate().unwrap();
let (_, other_seed) = MasterSeed::from_mnemonic_words(&other_words.to_string()).unwrap();
assert_ne!(a.to_string(), derive_cashu_mnemonic(&other_seed).unwrap().to_string());
assert_ne!(
a.to_string(),
derive_cashu_mnemonic(&other_seed).unwrap().to_string()
);
}
/// It must NOT be the node's own phrase. Restoring ecash into a
+1 -2
View File
@@ -664,8 +664,7 @@ impl Server {
.map(|(a, _)| *a)
.unwrap_or(0)
+ 1;
let delay =
(90u64 << attempts.min(10)).min(86_400);
let delay = (90u64 << attempts.min(10)).min(86_400);
notify_backoff.insert(
node.did.clone(),
(attempts, now + Duration::from_secs(delay)),
+8 -2
View File
@@ -765,7 +765,10 @@ mod tests {
let short = "01fc0ec0e59cd6fa";
let full = "01fc0ec0e59cd6fa01b7a88f8cd77fce81fd1e64bca67d752e984992b7a3c3a821";
assert!(is_truncated_v2_keyset_id(short));
assert!(full.starts_with(short), "short form must prefix the full id");
assert!(
full.starts_with(short),
"short form must prefix the full id"
);
// It must survive token validation so the swap path can repair it,
// rather than being rejected as malformed.
assert!(validate_keyset_id(short).is_ok());
@@ -784,7 +787,10 @@ mod tests {
let err = validate_keyset_id("00112233445566778899")
.expect_err("9-byte keyset id must be rejected");
let msg = err.to_string();
assert!(msg.contains("10-byte") || msg.contains("unsupported keyset id"), "{msg}");
assert!(
msg.contains("10-byte") || msg.contains("unsupported keyset id"),
"{msg}"
);
// Non-hex ids (the original base64 keyset format) are named as such
// rather than reported as a length problem.
+17 -8
View File
@@ -397,9 +397,8 @@ pub async fn load_accepted_mints(data_dir: &Path) -> Result<AcceptedMints> {
mints: vec![network.default_mint()],
}
} else {
serde_json::from_str(&content).with_context(|| {
format!("Accepted-mints file {} is damaged", path.display())
})?
serde_json::from_str(&content)
.with_context(|| format!("Accepted-mints file {} is damaged", path.display()))?
};
Ok(mints)
}
@@ -1521,7 +1520,10 @@ pub async fn restore_from_seed(data_dir: &Path, mint_url: &str) -> Result<Restor
let mint_key = match keys.key_for_amount(sig.amount) {
Ok(k) => k,
Err(e) => {
warn!("Restored a {} sat output with no matching key: {e:#}", sig.amount);
warn!(
"Restored a {} sat output with no matching key: {e:#}",
sig.amount
);
continue;
}
};
@@ -2312,7 +2314,11 @@ mod tests {
// mint — never the real coins.
save_network(dir, EcashNetwork::Testnet).await.unwrap();
let test_wallet = load_wallet(dir).await.unwrap();
assert_eq!(test_wallet.balance(), 0, "test wallet must not see real coins");
assert_eq!(
test_wallet.balance(),
0,
"test wallet must not see real coins"
);
assert!(test_wallet.mint_url.contains("testnut"));
assert!(load_accepted_mints(dir).await.unwrap().mints[0].contains("testnut"));
@@ -2341,7 +2347,6 @@ mod tests {
assert_eq!(back.proofs[0].proof.secret, "real");
}
#[tokio::test]
async fn a_damaged_wallet_file_fails_loudly_and_is_left_on_disk() {
let tmp = TempDir::new().unwrap();
@@ -2355,7 +2360,9 @@ mod tests {
// It must NOT read as an empty wallet: that is what caused the real
// balance to be overwritten with nothing on the next save.
let err = load_wallet(dir).await.expect_err("damaged wallet must error");
let err = load_wallet(dir)
.await
.expect_err("damaged wallet must error");
assert!(
err.to_string().contains("damaged"),
"error should name the problem: {err}"
@@ -2372,7 +2379,9 @@ mod tests {
std::fs::create_dir_all(dir.join("wallet")).unwrap();
std::fs::write(dir.join("wallet/ecash.json"), " \n").unwrap();
// A create that never got its first write is not damage.
let w = load_wallet(dir).await.expect("empty file is a fresh wallet");
let w = load_wallet(dir)
.await
.expect("empty file is a fresh wallet");
assert_eq!(w.balance(), 0);
}
+8 -5
View File
@@ -191,7 +191,10 @@ impl MintClient {
&self,
keyset_id: &str,
amounts: &[u64],
) -> Result<(Vec<BlindedMessageRequest>, Vec<(Vec<u8>, secp256k1::SecretKey, u64)>)> {
) -> Result<(
Vec<BlindedMessageRequest>,
Vec<(Vec<u8>, secp256k1::SecretKey, u64)>,
)> {
let derived = match &self.recovery {
Some(source) => match source.next_outputs(keyset_id, amounts.len()).await {
Ok(pairs) => Some(pairs),
@@ -320,9 +323,7 @@ impl MintClient {
.filter(|k| !k.keys.is_empty() && k.unit.eq_ignore_ascii_case("sat"))
// Prefer a keyset the mint will still sign with.
.max_by_key(|k| k.active)
.ok_or_else(|| {
anyhow::anyhow!("No active sat keyset found at mint {}", self.url)
})
.ok_or_else(|| anyhow::anyhow!("No active sat keyset found at mint {}", self.url))
}
// ── Mint quotes (NUT-04) ──
@@ -510,7 +511,9 @@ impl MintClient {
"The mint's fee ({fee} sat) consumes this whole amount — nothing would be left"
);
}
debug!("Reducing swap outputs {requested} -> {spendable} to cover a {fee} sat mint fee");
debug!(
"Reducing swap outputs {requested} -> {spendable} to cover a {fee} sat mint fee"
);
owned_targets = amount_to_denominations(spendable);
&owned_targets
} else {
+39 -22
View File
@@ -280,12 +280,18 @@ pub async fn establish_independent(data_dir: &Path) -> Result<EcashSeed> {
/// dangerous choice if the imported phrase turned out to be the one already
/// in use.
pub async fn import_mnemonic(data_dir: &Path, words: &str, confirm: bool) -> Result<EcashSeed> {
let mnemonic: bip39::Mnemonic = words.split_whitespace().collect::<Vec<_>>().join(" ").parse()
.map_err(|e| anyhow::anyhow!(
"That is not a valid BIP-39 recovery phrase: {e}. Check for typos — \
let mnemonic: bip39::Mnemonic = words
.split_whitespace()
.collect::<Vec<_>>()
.join(" ")
.parse()
.map_err(|e| {
anyhow::anyhow!(
"That is not a valid BIP-39 recovery phrase: {e}. Check for typos — \
every word must come from the BIP-39 word list, and the phrase as \
a whole carries a checksum."
))?;
)
})?;
if let Some(existing) = load_seed(data_dir).await? {
if existing.mnemonic == mnemonic {
@@ -322,19 +328,18 @@ async fn archive_seed(data_dir: &Path) -> Result<()> {
}
let stamp = chrono::Utc::now().format("%Y%m%dT%H%M%SZ");
let to = data_dir.join(format!("wallet/cashu_seed.replaced-{stamp}.json"));
fs::rename(&from, &to)
.await
.with_context(|| format!("Could not archive the previous ecash phrase to {}", to.display()))?;
fs::rename(&from, &to).await.with_context(|| {
format!(
"Could not archive the previous ecash phrase to {}",
to.display()
)
})?;
warn!("Previous ecash phrase archived to {}", to.display());
Ok(())
}
/// Write the seed file at 0600, creating the wallet directory if needed.
async fn write_seed(
data_dir: &Path,
mnemonic: &bip39::Mnemonic,
source: SeedSource,
) -> Result<()> {
async fn write_seed(data_dir: &Path, mnemonic: &bip39::Mnemonic, source: SeedSource) -> Result<()> {
let path = seed_path(data_dir);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
@@ -596,8 +601,7 @@ mod tests {
// A *different* master seed must not replace the phrase the existing
// proofs were minted under.
let (other_words, _) = MasterSeed::generate().unwrap();
let (_, other_master) =
MasterSeed::from_mnemonic_words(&other_words.to_string()).unwrap();
let (_, other_master) = MasterSeed::from_mnemonic_words(&other_words.to_string()).unwrap();
let third = establish_from_master(d, &other_master).await.unwrap();
assert_eq!(
first.words(),
@@ -615,10 +619,9 @@ mod tests {
// Stand in for the other wallet: a known phrase and what it derives.
let theirs: bip39::Mnemonic = TEST_MNEMONIC.parse().unwrap();
let expected =
EcashSeed::from_mnemonic(theirs.clone(), SeedSource::Imported)
.derive_output(V1_KEYSET, 3)
.unwrap();
let expected = EcashSeed::from_mnemonic(theirs.clone(), SeedSource::Imported)
.derive_output(V1_KEYSET, 3)
.unwrap();
let imported = import_mnemonic(d, TEST_MNEMONIC, false).await.unwrap();
assert_eq!(imported.source(), SeedSource::Imported);
@@ -644,7 +647,10 @@ mod tests {
let err = import_mnemonic(d, &other.to_string(), false)
.await
.expect_err("must not replace without confirmation");
assert!(err.to_string().contains("already has a backup phrase"), "{err}");
assert!(
err.to_string().contains("already has a backup phrase"),
"{err}"
);
assert_eq!(
load_seed(d).await.unwrap().unwrap().words(),
original_words,
@@ -660,7 +666,11 @@ mod tests {
let archived: Vec<_> = std::fs::read_dir(d.join("wallet"))
.unwrap()
.filter_map(|e| e.ok())
.filter(|e| e.file_name().to_string_lossy().starts_with("cashu_seed.replaced-"))
.filter(|e| {
e.file_name()
.to_string_lossy()
.starts_with("cashu_seed.replaced-")
})
.collect();
assert_eq!(archived.len(), 1, "the replaced phrase must be kept");
}
@@ -677,7 +687,11 @@ mod tests {
let archived = std::fs::read_dir(d.join("wallet"))
.unwrap()
.filter_map(|e| e.ok())
.filter(|e| e.file_name().to_string_lossy().starts_with("cashu_seed.replaced-"))
.filter(|e| {
e.file_name()
.to_string_lossy()
.starts_with("cashu_seed.replaced-")
})
.count();
assert_eq!(archived, 0);
}
@@ -718,7 +732,10 @@ mod tests {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mode = std::fs::metadata(seed_path(d)).unwrap().permissions().mode();
let mode = std::fs::metadata(seed_path(d))
.unwrap()
.permissions()
.mode();
assert_eq!(mode & 0o777, 0o600, "the ecash phrase must be owner-only");
}
}
+4 -2
View File
@@ -29,8 +29,10 @@ impl Router {
.with_context(|| format!("no address for {}", addr))?;
let tcp = TcpStream::connect_timeout(&resolved, std::time::Duration::from_secs(5))
.with_context(|| format!("TCP connect to {}", addr))?;
tcp.set_read_timeout(Some(std::time::Duration::from_secs(30))).ok();
tcp.set_write_timeout(Some(std::time::Duration::from_secs(30))).ok();
tcp.set_read_timeout(Some(std::time::Duration::from_secs(30)))
.ok();
tcp.set_write_timeout(Some(std::time::Duration::from_secs(30)))
.ok();
Ok(tcp)
}