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