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:
parent
144c4a2872
commit
83bb589ea6
@ -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"))
|
||||
|
||||
@ -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())
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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()
|
||||
})
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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!(
|
||||
|
||||
@ -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")
|
||||
}
|
||||
|
||||
|
||||
@ -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 \
|
||||
|
||||
@ -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"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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?;
|
||||
|
||||
@ -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"));
|
||||
}
|
||||
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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()]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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")
|
||||
}
|
||||
|
||||
|
||||
@ -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());
|
||||
}
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user