style: cargo fmt for v1.7.99-alpha release gate

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
archipelago 2026-06-17 19:50:46 -04:00
parent 144c4a2872
commit 83bb589ea6
28 changed files with 384 additions and 202 deletions

View File

@ -159,10 +159,7 @@ impl ApiHandler {
/// Seller side (#46): mint a Lightning invoice for a paid catalog item so a
/// buyer can pay from any external wallet. Path: GET /content/{id}/invoice.
/// Records a pending entitlement keyed by the invoice's payment hash.
pub(super) async fn handle_content_invoice(
&self,
path: &str,
) -> Result<Response<hyper::Body>> {
pub(super) async fn handle_content_invoice(&self, path: &str) -> Result<Response<hyper::Body>> {
let content_id = path
.strip_prefix("/content/")
.and_then(|s| s.strip_suffix("/invoice"))
@ -295,10 +292,7 @@ impl ApiHandler {
/// Seller side (#46): issue a fresh on-chain address for a paid catalog item
/// so a buyer can pay on-chain. Path: GET /content/{id}/onchain. Records a
/// pending entitlement keyed by the address; price doubles as expected amount.
pub(super) async fn handle_content_onchain(
&self,
path: &str,
) -> Result<Response<hyper::Body>> {
pub(super) async fn handle_content_onchain(&self, path: &str) -> Result<Response<hyper::Body>> {
let content_id = path
.strip_prefix("/content/")
.and_then(|s| s.strip_suffix("/onchain"))

View File

@ -382,8 +382,11 @@ impl RpcHandler {
// Surface a real reason instead of the generic sanitized error (#30):
// the dial already tries FIPS/mesh then falls back to Tor, so a failure
// here means the peer is genuinely unreachable on both transports.
let (response, transport) =
match crate::fips::dial::PeerRequest::new(fips_npub.as_deref(), onion, &path)
let (response, transport) = match crate::fips::dial::PeerRequest::new(
fips_npub.as_deref(),
onion,
&path,
)
.service(crate::settings::transport::PeerService::PeerFiles)
.header("X-Federation-DID", local_did)
.header("X-Payment-Token", token_str)
@ -576,8 +579,11 @@ impl RpcHandler {
let fips_npub = crate::federation::fips_npub_for_onion(&self.config.data_dir, onion).await;
let path = format!("/content/{}", content_id);
let (response, transport) =
match crate::fips::dial::PeerRequest::new(fips_npub.as_deref(), onion, &path)
let (response, transport) = match crate::fips::dial::PeerRequest::new(
fips_npub.as_deref(),
onion,
&path,
)
.service(crate::settings::transport::PeerService::PeerFiles)
.header("X-Federation-DID", local_did)
.header("X-Invoice-Hash", payment_hash.to_string())
@ -697,7 +703,10 @@ impl RpcHandler {
return Err(anyhow::anyhow!("Invalid v3 onion address"));
}
// Bitcoin addresses are alphanumeric; keep strictly so for safe path use.
if address.is_empty() || address.len() > 100 || !address.chars().all(|c| c.is_ascii_alphanumeric()) {
if address.is_empty()
|| address.len() > 100
|| !address.chars().all(|c| c.is_ascii_alphanumeric())
{
return Err(anyhow::anyhow!("Invalid address"));
}
@ -754,8 +763,11 @@ impl RpcHandler {
let fips_npub = crate::federation::fips_npub_for_onion(&self.config.data_dir, onion).await;
let path = format!("/content/{}", content_id);
let (response, transport) =
match crate::fips::dial::PeerRequest::new(fips_npub.as_deref(), onion, &path)
let (response, transport) = match crate::fips::dial::PeerRequest::new(
fips_npub.as_deref(),
onion,
&path,
)
.service(crate::settings::transport::PeerService::PeerFiles)
.header("X-Federation-DID", local_did)
.header("X-Onchain-Address", address.to_string())

View File

@ -268,12 +268,7 @@ impl RpcHandler {
let removed_pubkey = federation::load_nodes(&self.config.data_dir)
.await
.ok()
.and_then(|nodes| {
nodes
.into_iter()
.find(|n| n.did == did)
.map(|n| n.pubkey)
});
.and_then(|nodes| nodes.into_iter().find(|n| n.did == did).map(|n| n.pubkey));
let nodes = federation::remove_node(&self.config.data_dir, did).await?;
info!(did = %did, "Removed node from federation");

View File

@ -69,20 +69,23 @@ impl RpcHandler {
let federation_id = client.join(invite_code).await?;
// Try to label it from the federation meta (best-effort).
let name = client
.info()
.await
.ok()
.and_then(|i| {
let name = client.info().await.ok().and_then(|i| {
i.get(&federation_id)
.and_then(|e| e.get("meta"))
.and_then(|m| m.get("federation_name").or_else(|| m.get("federation_expiry_timestamp")))
.and_then(|m| {
m.get("federation_name")
.or_else(|| m.get("federation_expiry_timestamp"))
})
.and_then(|v| v.as_str())
.map(|s| s.to_string())
});
let mut reg = fedimint_client::load_registry(&self.config.data_dir).await?;
if !reg.federations.iter().any(|f| f.federation_id == federation_id) {
if !reg
.federations
.iter()
.any(|f| f.federation_id == federation_id)
{
reg.federations.push(JoinedFederation {
federation_id: federation_id.clone(),
name,

View File

@ -205,11 +205,7 @@ impl RpcHandler {
let payment_hash_hex = body
.get("r_hash")
.and_then(|v| v.as_str())
.and_then(|b64| {
base64::engine::general_purpose::STANDARD
.decode(b64)
.ok()
})
.and_then(|b64| base64::engine::general_purpose::STANDARD.decode(b64).ok())
.map(hex::encode)
.unwrap_or_default();
@ -317,9 +313,8 @@ impl RpcHandler {
.get("output_details")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter().any(|o| {
o.get("address").and_then(|a| a.as_str()) == Some(address)
})
arr.iter()
.any(|o| o.get("address").and_then(|a| a.as_str()) == Some(address))
})
.unwrap_or(false);
if pays_addr {

View File

@ -23,9 +23,8 @@ impl RpcHandler {
};
let (ollama_detected, models) = detect_ollama().await;
let claude_available = tokio::fs::metadata(
self.config.data_dir.join("secrets/claude-api-key"),
)
let claude_available =
tokio::fs::metadata(self.config.data_dir.join("secrets/claude-api-key"))
.await
.is_ok();
Ok(serde_json::json!({
@ -94,7 +93,10 @@ impl RpcHandler {
.get("fire_at")
.and_then(|v| v.as_i64())
.ok_or_else(|| anyhow::anyhow!("fire_at (unix seconds) is required"))?;
let contact_id = p.get("contact_id").and_then(|v| v.as_u64()).map(|v| v as u32);
let contact_id = p
.get("contact_id")
.and_then(|v| v.as_u64())
.map(|v| v as u32);
let channel = p.get("channel").and_then(|v| v.as_u64()).map(|v| v as u8);
if contact_id.is_none() && channel.is_none() {
anyhow::bail!("either contact_id or channel is required");
@ -104,7 +106,10 @@ impl RpcHandler {
let svc = service
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Mesh service not running"))?;
let msg = svc.scheduler.add(contact_id, channel, body, fire_at).await?;
let msg = svc
.scheduler
.add(contact_id, channel, body, fire_at)
.await?;
Ok(serde_json::to_value(msg)?)
}
@ -157,7 +162,9 @@ async fn detect_ollama() -> (bool, Vec<String>) {
.map(|arr| {
arr.iter()
.filter_map(|m| {
m.get("name").and_then(|n| n.as_str()).map(|s| s.to_string())
m.get("name")
.and_then(|n| n.as_str())
.map(|s| s.to_string())
})
.collect()
})

View File

@ -116,7 +116,10 @@ impl RpcHandler {
{
config.announce_block_headers = announce;
}
if let Some(receive) = params.get("receive_block_headers").and_then(|v| v.as_bool()) {
if let Some(receive) = params
.get("receive_block_headers")
.and_then(|v| v.as_bool())
{
config.receive_block_headers = receive;
}

View File

@ -28,8 +28,14 @@ impl RpcHandler {
})
};
if let Some(obj) = value.as_object_mut() {
obj.insert("announce_block_headers".into(), config.announce_block_headers.into());
obj.insert("receive_block_headers".into(), config.receive_block_headers.into());
obj.insert(
"announce_block_headers".into(),
config.announce_block_headers.into(),
);
obj.insert(
"receive_block_headers".into(),
config.receive_block_headers.into(),
);
}
Ok(value)
}

View File

@ -469,7 +469,10 @@ async fn wait_for_stack_containers(
PODMAN_STACK_PROBE_TIMEOUT,
)
.await;
pending.push(format!("{}=restarting({}/{})", container, *attempts, MAX_RESTARTS));
pending.push(format!(
"{}=restarting({}/{})",
container, *attempts, MAX_RESTARTS
));
} else {
let logs = stack_container_logs(container, 40).await;
install_log(&format!(
@ -1997,11 +2000,7 @@ async fn ensure_netbird_tls_cert(host_ip: &str) -> Result<()> {
Ok(())
}
async fn write_netbird_config_files(
host_ip: &str,
lan_ip: &str,
resolver_ip: &str,
) -> Result<()> {
async fn write_netbird_config_files(host_ip: &str, lan_ip: &str, resolver_ip: &str) -> Result<()> {
// netbird's dashboard uses window.crypto.subtle (OIDC PKCE), which browsers
// only expose in a SECURE context — so the proxy serves HTTPS and every
// origin here is https (issue #15: over plain http the dashboard threw
@ -2187,7 +2186,10 @@ async fn detect_netbird_public_host_ip() -> Option<String> {
.await
.ok()?;
let stdout = String::from_utf8_lossy(&output.stdout);
let ips: Vec<&str> = stdout.split_whitespace().filter(|s| s.contains('.')).collect();
let ips: Vec<&str> = stdout
.split_whitespace()
.filter(|s| s.contains('.'))
.collect();
// Prefer the LAN address as the canonical origin — that's what users browse
// to on the local network. Baking the Tailscale 100.x address here broke

View File

@ -398,8 +398,7 @@ impl RpcHandler {
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let secret_phrase = passphrase.unwrap_or_else(|| password.clone());
let reveal =
crate::seed::load_seed_encrypted(&self.config.data_dir, &secret_phrase).await;
let reveal = crate::seed::load_seed_encrypted(&self.config.data_dir, &secret_phrase).await;
password.zeroize();
let mnemonic = reveal.map_err(|_| {
anyhow::anyhow!(

View File

@ -92,7 +92,10 @@ fn cmd_sign(path: &str) -> Result<()> {
let obj = value.as_object_mut().expect("checked above");
obj.insert("signature".into(), serde_json::Value::String(signature));
obj.insert("signed_by".into(), serde_json::Value::String(signed_by.clone()));
obj.insert(
"signed_by".into(),
serde_json::Value::String(signed_by.clone()),
);
let pretty = serde_json::to_string_pretty(&value).context("serialize signed document")?;
let tmp = format!("{path}.tmp");
@ -107,8 +110,8 @@ fn cmd_sign(path: &str) -> Result<()> {
/// Derive the release-root signing key from the mnemonic in env/stdin.
fn load_release_root_key() -> Result<SigningKey> {
let phrase = read_mnemonic()?;
let (_mnemonic, seed) =
MasterSeed::from_mnemonic_words(phrase.trim()).context("invalid release master mnemonic")?;
let (_mnemonic, seed) = MasterSeed::from_mnemonic_words(phrase.trim())
.context("invalid release master mnemonic")?;
seed::derive_release_root_ed25519(&seed).context("derive release-root")
}

View File

@ -281,9 +281,15 @@ async fn fetch_one(client: &reqwest::Client, url: &str) -> anyhow::Result<AppCat
crate::trust::SignatureStatus::Unsigned => {
debug!("app-catalog: unsigned (accepted during migration window)");
}
crate::trust::SignatureStatus::Verified { signer_did, anchored } => {
crate::trust::SignatureStatus::Verified {
signer_did,
anchored,
} => {
if anchored {
info!("app-catalog: release-root signature verified ({})", signer_did);
info!(
"app-catalog: release-root signature verified ({})",
signer_did
);
} else {
warn!(
"app-catalog: signature self-consistent but release-root anchor \

View File

@ -235,7 +235,9 @@ fn build_fingerprint_stamp_path(data_dir: &Path, tag: &str) -> PathBuf {
.chars()
.map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
.collect();
data_dir.join(".image-build").join(format!("{safe}.fingerprint"))
data_dir
.join(".image-build")
.join(format!("{safe}.fingerprint"))
}
async fn chown_for_rootless_container(uid_gid: &str, path: &str) -> Result<()> {
@ -362,7 +364,16 @@ async fn ensure_running_container_ownership(name: &str) -> bool {
// Repair inside the container's userns — podman maps to the right host uid.
let chown = tokio::process::Command::new("podman")
.args(["exec", "-u", "0", name, "chown", "-R", &format!("{uid}:{gid}"), dest])
.args([
"exec",
"-u",
"0",
name,
"chown",
"-R",
&format!("{uid}:{gid}"),
dest,
])
.output()
.await;
match chown {
@ -378,7 +389,9 @@ async fn ensure_running_container_ownership(name: &str) -> bool {
"volume ownership repair failed: {}",
String::from_utf8_lossy(&o.stderr).trim()
),
Err(e) => tracing::warn!(container = %name, dest, "volume ownership repair errored: {e}"),
Err(e) => {
tracing::warn!(container = %name, dest, "volume ownership repair errored: {e}")
}
}
}
repaired
@ -4634,10 +4647,15 @@ app:
#[test]
fn build_fingerprint_stamp_path_sanitizes_tag() {
let p = build_fingerprint_stamp_path(Path::new("/var/lib/archipelago"), "localhost/bitcoin-ui:local");
let p = build_fingerprint_stamp_path(
Path::new("/var/lib/archipelago"),
"localhost/bitcoin-ui:local",
);
assert_eq!(
p,
PathBuf::from("/var/lib/archipelago/.image-build/localhost_bitcoin_ui_local.fingerprint")
PathBuf::from(
"/var/lib/archipelago/.image-build/localhost_bitcoin_ui_local.fingerprint"
)
);
}
}

View File

@ -65,7 +65,9 @@ pub(super) async fn run_assist(
"AssistQuery denied — sender not permitted by assistant policy"
);
// Silent on the wire (no airtime spent on denials); surface to the UI.
let _ = state.event_tx.send(super::super::types::MeshEvent::AssistResponseReady {
let _ = state
.event_tx
.send(super::super::types::MeshEvent::AssistResponseReady {
req_id,
to_contact_id: asker,
error: Some("denied".to_string()),
@ -77,12 +79,17 @@ pub(super) async fn run_assist(
{
let mut inflight = state.assist_inflight.write().await;
if !inflight.insert(asker) {
warn!(from = asker, "AssistQuery dropped — asker already has one in flight");
warn!(
from = asker,
"AssistQuery dropped — asker already has one in flight"
);
return;
}
}
let _ = state.event_tx.send(super::super::types::MeshEvent::AssistQueryReceived {
let _ = state
.event_tx
.send(super::super::types::MeshEvent::AssistQueryReceived {
from_contact_id: asker,
prompt: prompt.clone(),
});
@ -92,7 +99,11 @@ pub(super) async fn run_assist(
(a.backend.clone(), a.model.clone())
};
let is_claude = backend == "claude";
let default_model = if is_claude { CLAUDE_DEFAULT_MODEL } else { DEFAULT_MODEL };
let default_model = if is_claude {
CLAUDE_DEFAULT_MODEL
} else {
DEFAULT_MODEL
};
let model = model_override
.or(configured_model)
.unwrap_or_else(|| default_model.to_string());
@ -108,7 +119,9 @@ pub(super) async fn run_assist(
match result {
Ok(answer) => {
send_reply(&state, &reply, req_id, &answer).await;
let _ = state.event_tx.send(super::super::types::MeshEvent::AssistResponseReady {
let _ = state
.event_tx
.send(super::super::types::MeshEvent::AssistResponseReady {
req_id,
to_contact_id: asker,
error: None,
@ -117,7 +130,9 @@ pub(super) async fn run_assist(
Err(e) => {
warn!(req_id, "AI query failed: {}", e);
send_failure(&state, &reply, req_id, "AI unavailable").await;
let _ = state.event_tx.send(super::super::types::MeshEvent::AssistResponseReady {
let _ = state
.event_tx
.send(super::super::types::MeshEvent::AssistResponseReady {
req_id,
to_contact_id: asker,
error: Some(e.to_string()),
@ -219,7 +234,10 @@ async fn send_typed_chunks(state: &Arc<MeshState>, dest_contact_id: u32, req_id:
let chunks: Vec<String> = if chars.is_empty() {
vec![String::new()]
} else {
chars.chunks(CHUNK_CHARS).map(|c| c.iter().collect()).collect()
chars
.chunks(CHUNK_CHARS)
.map(|c| c.iter().collect())
.collect()
};
let last = chunks.len().saturating_sub(1);
for (i, chunk) in chunks.into_iter().enumerate() {
@ -337,7 +355,10 @@ async fn call_claude(data_dir: &Path, model: &str, prompt: &str) -> anyhow::Resu
let text = json
.get("content")
.and_then(|c| c.as_array())
.and_then(|arr| arr.iter().find_map(|b| b.get("text").and_then(|t| t.as_str())))
.and_then(|arr| {
arr.iter()
.find_map(|b| b.get("text").and_then(|t| t.as_str()))
})
.unwrap_or("")
.to_string();
if text.trim().is_empty() {

View File

@ -340,9 +340,7 @@ async fn handle_channel_payload(
// seeded peer instead of creating a duplicate chat thread. Not stored as
// a chat message.
if let Ok(text) = std::str::from_utf8(payload) {
if let Some((did, ed_hex, x_hex)) =
super::super::protocol::parse_identity_broadcast(text)
{
if let Some((did, ed_hex, x_hex)) = super::super::protocol::parse_identity_broadcast(text) {
// Ignore our own identity echoed back by the radio/channel.
if ed_hex.eq_ignore_ascii_case(&state.our_ed_pubkey_hex) {
return;

View File

@ -433,7 +433,10 @@ pub(super) async fn run_mesh_session(
our_ed_pubkey_hex,
our_x25519_pubkey_hex,
);
if let Err(e) = device.send_channel_text(0, identity_advert.as_bytes()).await {
if let Err(e) = device
.send_channel_text(0, identity_advert.as_bytes())
.await
{
warn!("Failed to broadcast archipelago identity: {}", e);
}

View File

@ -165,7 +165,11 @@ async fn fire_due(scheduler: &Arc<MeshScheduler>, state: &Arc<MeshState>) {
if failed.contains(&m.id) {
m.attempts += 1;
if m.attempts >= MAX_ATTEMPTS {
warn!(id = m.id, attempts = m.attempts, "Dropping undeliverable scheduled message");
warn!(
id = m.id,
attempts = m.attempts,
"Dropping undeliverable scheduled message"
);
to_remove.push(m.id);
}
}

View File

@ -292,9 +292,9 @@ impl Server {
sm.update_data(data).await;
}
Ok(_) => {}
Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {
continue
}
Err(tokio::sync::broadcast::error::RecvError::Lagged(
_,
)) => continue,
Err(_) => break, // sender dropped → mesh stopped
}
}

View File

@ -120,7 +120,8 @@ impl IrohProvider {
// The event sender gates each request through the ecash `streaming` layer
// — free by default, paid only if the operator priced `content-download`
// (Networking Profits → Settings). It also hard-disables peer writes.
let event_sender = super::paid::gated_event_sender(data_dir.to_path_buf(), (*store).clone());
let event_sender =
super::paid::gated_event_sender(data_dir.to_path_buf(), (*store).clone());
let blobs = BlobsProtocol::new(&store, Some(event_sender));
// Shape-A paid negotiation rides a second ALPN on the same endpoint so a
// downloader can pay (open a session) before the blob-GET above serves it.

View File

@ -108,7 +108,9 @@ pub async fn init(
if enabled {
warn!("swarm: swarm_enabled set but binary built without the `iroh-swarm` feature — staying origin-only");
}
let _ = RUNTIME.set(SwarmRuntime { providers: Vec::new() });
let _ = RUNTIME.set(SwarmRuntime {
providers: Vec::new(),
});
return Ok(());
}
@ -123,13 +125,10 @@ pub async fn init(
return Ok(());
}
let discovery: Arc<dyn iroh_provider::ProviderDiscovery> =
Arc::new(iroh_provider::NostrSeedDiscovery::new(
relays.to_vec(),
tor_proxy.map(str::to_string),
));
let provider =
Arc::new(iroh_provider::IrohProvider::new(data_dir, Some(discovery)).await?);
let discovery: Arc<dyn iroh_provider::ProviderDiscovery> = Arc::new(
iroh_provider::NostrSeedDiscovery::new(relays.to_vec(), tor_proxy.map(str::to_string)),
);
let provider = Arc::new(iroh_provider::IrohProvider::new(data_dir, Some(discovery)).await?);
info!(
"swarm: iroh provider active (endpoint {}) — swarm-assist enabled, origin always wins",
provider.endpoint_id()
@ -231,7 +230,10 @@ where
}
}
debug!("swarm: no provider served {} — falling back to origin", digest);
debug!(
"swarm: no provider served {} — falling back to origin",
digest
);
origin().await?;
Ok(FetchSource::Origin)
}
@ -248,7 +250,11 @@ mod tests {
use std::sync::atomic::{AtomicBool, Ordering};
fn digest_of(bytes: &[u8]) -> ContentDigest {
ContentDigest::parse(&format!("blake3:{}", crate::content_hash::blake3_hex(bytes))).unwrap()
ContentDigest::parse(&format!(
"blake3:{}",
crate::content_hash::blake3_hex(bytes)
))
.unwrap()
}
/// Provider that writes a fixed payload (which may or may not match).
@ -295,7 +301,10 @@ mod tests {
.await
.unwrap();
assert_eq!(src, FetchSource::Swarm);
assert!(!origin_ran.load(Ordering::SeqCst), "origin must not run on swarm hit");
assert!(
!origin_ran.load(Ordering::SeqCst),
"origin must not run on swarm hit"
);
assert_eq!(tokio::fs::read(&dest).await.unwrap(), content);
}
@ -316,7 +325,11 @@ mod tests {
})
.await
.unwrap();
assert_eq!(src, FetchSource::Origin, "tampered swarm bytes must not be accepted");
assert_eq!(
src,
FetchSource::Origin,
"tampered swarm bytes must not be accepted"
);
assert_eq!(tokio::fs::read(&dest).await.unwrap(), content);
}
@ -343,8 +356,14 @@ mod tests {
let content = b"second wins".to_vec();
let digest = digest_of(&content);
let providers = vec![
arc(FixedProvider { name: "miss", payload: None }),
arc(FixedProvider { name: "hit", payload: Some(content.clone()) }),
arc(FixedProvider {
name: "miss",
payload: None,
}),
arc(FixedProvider {
name: "hit",
payload: Some(content.clone()),
}),
];
let src = fetch_content_addressed(&digest, &providers, &dest, || async {
tokio::fs::write(&dest, b"origin").await?;

View File

@ -166,7 +166,9 @@ impl ProtocolHandler for PaidBlobsProtocol {
},
};
let bytes = serde_json::to_vec(&response).map_err(AcceptError::from_err)?;
send.write_all(&bytes).await.map_err(AcceptError::from_err)?;
send.write_all(&bytes)
.await
.map_err(AcceptError::from_err)?;
send.finish().map_err(AcceptError::from_err)?;
}
Ok(())
@ -192,7 +194,9 @@ pub async fn negotiate_access(
match negotiate_inner(endpoint, data_dir, peer, blake3_hex, policy).await {
Ok(proceed) => proceed,
Err(e) => {
tracing::debug!("paid-alpn: negotiation with {peer} failed ({e}) — proceeding (gate decides)");
tracing::debug!(
"paid-alpn: negotiation with {peer} failed ({e}) — proceeding (gate decides)"
);
true
}
}
@ -265,7 +269,10 @@ mod tests {
token: None,
};
let json = serde_json::to_string(&req).unwrap();
assert!(!json.contains("token"), "absent token must be omitted: {json}");
assert!(
!json.contains("token"),
"absent token must be omitted: {json}"
);
let back: PaidRequest = serde_json::from_str(&json).unwrap();
assert_eq!(back.want, "abcd");
assert!(back.token.is_none());
@ -277,7 +284,8 @@ mod tests {
want: "ff".into(),
token: Some("cashuAbc".into()),
};
let back: PaidRequest = serde_json::from_str(&serde_json::to_string(&req).unwrap()).unwrap();
let back: PaidRequest =
serde_json::from_str(&serde_json::to_string(&req).unwrap()).unwrap();
assert_eq!(back.token.as_deref(), Some("cashuAbc"));
}

View File

@ -88,7 +88,8 @@ pub async fn auto_pay_token(
return Ok(None);
}
match ecash::build_payment_token(data_dir, accepted_mints, price_sats, policy.max_fee_sats).await
match ecash::build_payment_token(data_dir, accepted_mints, price_sats, policy.max_fee_sats)
.await
{
Ok(token) => Ok(Some(token)),
Err(e) => {

View File

@ -191,7 +191,10 @@ mod tests {
let hash = "b".repeat(64);
let json = serde_json::to_string(&advertisement_filter(&hash)).unwrap();
assert!(json.contains(&hash), "filter must target the hash d-tag");
assert!(json.contains("30081"), "filter must constrain the seed kind");
assert!(
json.contains("30081"),
"filter must constrain the seed kind"
);
}
#[test]
@ -212,10 +215,19 @@ mod tests {
let a = Keys::generate();
let b = Keys::generate();
let hash = "d".repeat(64);
let e1 = advertisement_builder(&hash, "endpoint-A").sign_with_keys(&a).unwrap();
let e2 = advertisement_builder(&hash, "endpoint-A").sign_with_keys(&b).unwrap();
let e3 = advertisement_builder(&hash, "endpoint-B").sign_with_keys(&b).unwrap();
let e1 = advertisement_builder(&hash, "endpoint-A")
.sign_with_keys(&a)
.unwrap();
let e2 = advertisement_builder(&hash, "endpoint-A")
.sign_with_keys(&b)
.unwrap();
let e3 = advertisement_builder(&hash, "endpoint-B")
.sign_with_keys(&b)
.unwrap();
let ids = endpoint_ids_from_events([&e1, &e2, &e3]);
assert_eq!(ids, vec!["endpoint-A".to_string(), "endpoint-B".to_string()]);
assert_eq!(
ids,
vec!["endpoint-A".to_string(), "endpoint-B".to_string()]
);
}
}

View File

@ -28,9 +28,7 @@ pub fn ed25519_pubkey_from_did_key(did: &str) -> Result<VerifyingKey> {
if decoded.len() != 34 || decoded[0..2] != ED25519_MULTICODEC {
return Err(anyhow!("not an Ed25519 did:key (bad multicodec prefix)"));
}
let arr: [u8; 32] = decoded[2..]
.try_into()
.expect("length checked above");
let arr: [u8; 32] = decoded[2..].try_into().expect("length checked above");
VerifyingKey::from_bytes(&arr).context("invalid Ed25519 public key in did:key")
}

View File

@ -60,7 +60,13 @@ pub fn verify_detached(doc: &Value) -> Result<SignatureStatus> {
let signed_by = obj
.get(SIGNED_BY_FIELD)
.and_then(Value::as_str)
.ok_or_else(|| anyhow!("signed document has `{}` but no `{}`", SIGNATURE_FIELD, SIGNED_BY_FIELD))?;
.ok_or_else(|| {
anyhow!(
"signed document has `{}` but no `{}`",
SIGNATURE_FIELD,
SIGNED_BY_FIELD
)
})?;
let signer = did::ed25519_pubkey_from_did_key(signed_by)
.with_context(|| format!("invalid `{}` did:key", SIGNED_BY_FIELD))?;
@ -174,7 +180,10 @@ mod tests {
#[test]
fn tampered_payload_is_rejected() {
let mut signed = sign_into(&test_key(), json!({"schema": 1, "n": 42}));
signed.as_object_mut().unwrap().insert("n".into(), json!(43));
signed
.as_object_mut()
.unwrap()
.insert("n".into(), json!(43));
assert!(verify_detached(&signed).is_err());
}

View File

@ -489,8 +489,12 @@ pub async fn swap_between_mints(
// The pay leg never completed — record the route failure so future
// payments can prefer a route with a track record.
record_swap_failure(data_dir, from_mint, to_mint).await;
return Err(e)
.with_context(|| format!("melting source proofs at {} to pay target invoice", from_mint));
return Err(e).with_context(|| {
format!(
"melting source proofs at {} to pay target invoice",
from_mint
)
});
}
// Persist the spend BEFORE claiming so a crash can't double-spend, and
@ -533,7 +537,10 @@ pub async fn swap_between_mints(
wallet.record_tx(
TransactionType::Mint,
minted,
&format!("Cross-mint swap {}{}: claimed {} sats", from_mint, to_mint, minted),
&format!(
"Cross-mint swap {}→{}: claimed {} sats",
from_mint, to_mint, minted
),
to_mint,
from_mint,
);
@ -680,7 +687,11 @@ pub enum PaymentPlan {
/// Pure and synchronous so it can be unit-tested without a live mint. It does
/// not know swap fees; `swap_between_mints` enforces the fee cap and bails (→
/// origin fallback) if the chosen source can't cover amount + fee.
fn plan_payment(holdings: &[(String, u64)], accepted: &[(String, bool)], amount: u64) -> PaymentPlan {
fn plan_payment(
holdings: &[(String, u64)],
accepted: &[(String, bool)],
amount: u64,
) -> PaymentPlan {
let norm = |s: &str| s.trim_end_matches('/').to_string();
let home = norm(&default_mint_url());
let held = |mint: &str| -> u64 {
@ -692,10 +703,8 @@ fn plan_payment(holdings: &[(String, u64)], accepted: &[(String, bool)], amount:
};
// 1. Direct: any accepted mint we already hold enough on. Prefer home.
let mut direct: Vec<&(String, bool)> = accepted
.iter()
.filter(|(m, _)| held(m) >= amount)
.collect();
let mut direct: Vec<&(String, bool)> =
accepted.iter().filter(|(m, _)| held(m) >= amount).collect();
direct.sort_by_key(|(m, _)| norm(m) != home); // home (false) sorts first
if let Some((mint, _)) = direct.first() {
return PaymentPlan::Direct {
@ -761,7 +770,10 @@ pub async fn build_payment_token(
match plan_payment(&holdings, &accepted, amount_sats) {
PaymentPlan::Direct { mint_url } => {
debug!("Payment plan: direct from {} for {} sats", mint_url, amount_sats);
debug!(
"Payment plan: direct from {} for {} sats",
mint_url, amount_sats
);
send_token_at(data_dir, &mint_url, amount_sats).await
}
PaymentPlan::Swap { from_mint, to_mint } => {
@ -844,7 +856,10 @@ pub async fn resume_pending_swaps(data_dir: &Path) -> Result<u64> {
let to = match MintClient::new(&swap.to_mint) {
Ok(c) => c,
Err(e) => {
warn!("resume_pending_swaps: bad target mint {}: {}", swap.to_mint, e);
warn!(
"resume_pending_swaps: bad target mint {}: {}",
swap.to_mint, e
);
continue;
}
};
@ -883,7 +898,10 @@ pub async fn resume_pending_swaps(data_dir: &Path) -> Result<u64> {
swap.from_mint, swap.to_mint, minted
);
}
Err(e) => warn!("resume_pending_swaps: claim failed for {}: {}", swap.mint_quote_id, e),
Err(e) => warn!(
"resume_pending_swaps: claim failed for {}: {}",
swap.mint_quote_id, e
),
},
"ISSUED" => {
// Already claimed on a previous run — drop the journal entry.
@ -940,14 +958,20 @@ async fn save_swap_liquidity(data_dir: &Path, liq: &SwapLiquidity) {
/// Record that a swap route succeeded (best-effort; never fails the caller).
async fn record_swap_success(data_dir: &Path, from_mint: &str, to_mint: &str) {
let mut liq = load_swap_liquidity(data_dir).await;
liq.routes.entry(route_key(from_mint, to_mint)).or_default().successes += 1;
liq.routes
.entry(route_key(from_mint, to_mint))
.or_default()
.successes += 1;
save_swap_liquidity(data_dir, &liq).await;
}
/// Record that a swap route failed (best-effort; never fails the caller).
async fn record_swap_failure(data_dir: &Path, from_mint: &str, to_mint: &str) {
let mut liq = load_swap_liquidity(data_dir).await;
liq.routes.entry(route_key(from_mint, to_mint)).or_default().failures += 1;
liq.routes
.entry(route_key(from_mint, to_mint))
.or_default()
.failures += 1;
save_swap_liquidity(data_dir, &liq).await;
}
@ -1574,17 +1598,38 @@ mod tests {
wallet.add_proofs(
"http://mint-a",
vec![
Proof { amount: 10, id: "k".into(), secret: "s1".into(), c: "c".into() },
Proof { amount: 5, id: "k".into(), secret: "s2".into(), c: "c".into() },
Proof {
amount: 10,
id: "k".into(),
secret: "s1".into(),
c: "c".into(),
},
Proof {
amount: 5,
id: "k".into(),
secret: "s2".into(),
c: "c".into(),
},
],
);
wallet.add_proofs(
"http://mint-b",
vec![Proof { amount: 7, id: "k".into(), secret: "s3".into(), c: "c".into() }],
vec![Proof {
amount: 7,
id: "k".into(),
secret: "s3".into(),
c: "c".into(),
}],
);
wallet.proofs[1].spent = true; // exclude the 5 on mint-a
let by_mint = wallet.spendable_by_mint();
assert_eq!(by_mint, vec![("http://mint-a".to_string(), 10), ("http://mint-b".to_string(), 7)]);
assert_eq!(
by_mint,
vec![
("http://mint-a".to_string(), 10),
("http://mint-b".to_string(), 7)
]
);
}
#[test]
@ -1606,7 +1651,9 @@ mod tests {
let accepted = vec![("https://b".into(), true)];
assert_eq!(
plan_payment(&holdings, &accepted, 50),
PaymentPlan::Direct { mint_url: "https://b".into() }
PaymentPlan::Direct {
mint_url: "https://b".into()
}
);
}
@ -1617,7 +1664,10 @@ mod tests {
let accepted = vec![("https://b".into(), true)];
assert_eq!(
plan_payment(&holdings, &accepted, 50),
PaymentPlan::Swap { from_mint: "https://a".into(), to_mint: "https://b".into() }
PaymentPlan::Swap {
from_mint: "https://a".into(),
to_mint: "https://b".into()
}
);
}
@ -1626,7 +1676,10 @@ mod tests {
// Seeder accepts only B, but B is not trusted → no swap, insufficient.
let holdings = vec![("https://a".into(), 100)];
let accepted = vec![("https://b".into(), false)];
assert_eq!(plan_payment(&holdings, &accepted, 50), PaymentPlan::Insufficient);
assert_eq!(
plan_payment(&holdings, &accepted, 50),
PaymentPlan::Insufficient
);
}
#[test]
@ -1635,7 +1688,10 @@ mod tests {
// we hold neither accepted mint directly.
let holdings = vec![("https://a".into(), 30), ("https://c".into(), 30)];
let accepted = vec![("https://b".into(), true)];
assert_eq!(plan_payment(&holdings, &accepted, 50), PaymentPlan::Insufficient);
assert_eq!(
plan_payment(&holdings, &accepted, 50),
PaymentPlan::Insufficient
);
}
#[test]
@ -1646,7 +1702,9 @@ mod tests {
let accepted = vec![("https://b".into(), true)];
assert_eq!(
plan_payment(&holdings, &accepted, 50),
PaymentPlan::Direct { mint_url: "https://b".into() }
PaymentPlan::Direct {
mint_url: "https://b".into()
}
);
}
@ -1744,7 +1802,10 @@ mod tests {
record_swap_success(tmp.path(), "https://src", "https://liquid").await;
// Seeder accepts both non-home mints; we only hold "https://src".
let accepted = vec![("https://dry".into(), true), ("https://liquid".into(), true)];
let accepted = vec![
("https://dry".into(), true),
("https://liquid".into(), true),
];
let holdings = vec![("https://src".to_string(), 1000u64)];
// Mirror build_payment_token's ordering step, then plan.

View File

@ -88,7 +88,11 @@ pub async fn ensure_default_federation(data_dir: &Path) -> Result<()> {
}
};
let mut reg = load_registry(data_dir).await?;
if !reg.federations.iter().any(|f| f.federation_id == federation_id) {
if !reg
.federations
.iter()
.any(|f| f.federation_id == federation_id)
{
reg.federations.push(JoinedFederation {
federation_id,
name: None,