From ce3e1c1416b89a745d4798cb0022460f51f7944d Mon Sep 17 00:00:00 2001 From: Mike Rahel Date: Mon, 27 Apr 2026 03:34:02 -0400 Subject: [PATCH 1/4] Auto-reconnect Lightning peers and document service management MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Lightning node had no way to keep critical peer connections alive after a TCP drop — once a peer disconnected, channels became unusable until the user manually called ln_connect again. This is the same gap that left an LN channel offline after a recent reboot even though both nodes were healthy. Add a persistent_peers config field on LightningConfig listing peers the node should keep connected (pubkey@host:port). On startup, after the LDK background processor is running, spawn a reconnector that ticks every 60s, walks the peer list, and dials any persistent peer not currently in peer_manager().list_peers(). Hostnames are resolved on each tick so DHCP/DNS changes on the LAN don't permanently break reconnection. Also document service management since users without an AI assistant need to know how to start and stop the node: - README gains a Stop section (Ctrl+C, RPC stop, kill, dashboard Settings page) and Auto-Start sections for macOS launchd and Linux systemd. - A template launchd plist lands in config/com.bitcoinwolfe.node.plist with placeholder paths for the user to fill in. The plist uses KeepAlive with SuccessfulExit=false so launchd restarts the node on crashes but respects intentional shutdowns. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 91 ++++++++++++++++++++++++++++++ config/com.bitcoinwolfe.node.plist | 42 ++++++++++++++ crates/wolfe-node/src/main.rs | 67 ++++++++++++++++++++++ crates/wolfe-types/src/config.rs | 3 + 4 files changed, 203 insertions(+) create mode 100644 config/com.bitcoinwolfe.node.plist diff --git a/README.md b/README.md index 9ef09b5..44ed971 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,92 @@ The binary is at `target/release/wolfe`. ./target/release/wolfe --config my-config.toml start ``` +### Stop + +The node shuts down gracefully via any of these methods: + +```bash +# From the terminal that started it +Ctrl+C + +# Via JSON-RPC +curl -s http://127.0.0.1:8332/ \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","id":1,"method":"stop"}' + +# Via signal +kill $(pgrep -f "wolfe start") +``` + +You can also stop from the **Settings** page in the web dashboard. + +All methods trigger the same graceful shutdown: Lightning channel state is persisted, the consensus engine is interrupted cleanly, and peer connections are closed. + +### Auto-Start on macOS (launchd) + +To have BitcoinWolfe start automatically on boot and restart after crashes: + +```bash +# 1. Copy the template plist +cp config/com.bitcoinwolfe.node.plist ~/Library/LaunchAgents/ + +# 2. Edit it — replace the placeholder paths with your actual paths: +# __WOLFE_BINARY__ → full path to the wolfe binary (e.g. /Users/you/BitcoinWolfe/target/release/wolfe) +# __WOLFE_DIR__ → full path to the project directory (e.g. /Users/you/BitcoinWolfe) +nano ~/Library/LaunchAgents/com.bitcoinwolfe.node.plist + +# 3. Load the service +launchctl load ~/Library/LaunchAgents/com.bitcoinwolfe.node.plist + +# 4. Verify it's running +launchctl list | grep bitcoinwolfe +curl -s http://127.0.0.1:8332/api/peers | head -c 100 +``` + +To manage the service: + +```bash +# Stop the service (and prevent auto-restart) +launchctl unload ~/Library/LaunchAgents/com.bitcoinwolfe.node.plist + +# Reload after config changes +launchctl unload ~/Library/LaunchAgents/com.bitcoinwolfe.node.plist +launchctl load ~/Library/LaunchAgents/com.bitcoinwolfe.node.plist +``` + +> **Note:** The plist uses `KeepAlive` with `SuccessfulExit = false`, so launchd will restart the node if it crashes but not if you stop it intentionally via RPC `stop`, `Ctrl+C`, or the dashboard. + +### Auto-Start on Linux (systemd) + +```bash +# Create a service file +sudo tee /etc/systemd/system/bitcoinwolfe.service << 'EOF' +[Unit] +Description=BitcoinWolfe Node +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=YOUR_USER +WorkingDirectory=/path/to/BitcoinWolfe +ExecStart=/path/to/BitcoinWolfe/target/release/wolfe start +Restart=on-failure +RestartSec=10 + +[Install] +WantedBy=multi-user.target +EOF + +# Enable and start +sudo systemctl daemon-reload +sudo systemctl enable bitcoinwolfe +sudo systemctl start bitcoinwolfe + +# Check status +sudo systemctl status bitcoinwolfe +``` + --- ## CLI Usage @@ -170,6 +256,11 @@ accept_inbound_channels = true min_channel_size_sat = 20000 max_channel_size_sat = 16777215 # rapid_gossip_sync_url = "https://rapidsync.lightningdevkit.org/snapshot" + +# Lightning peers that the node should keep connected. The reconnector +# checks every 60 seconds and dials any listed peer that's currently +# offline. Hostnames are re-resolved on each tick. +# persistent_peers = ["02abc...@host.example.com:9735"] ``` ### Nostr diff --git a/config/com.bitcoinwolfe.node.plist b/config/com.bitcoinwolfe.node.plist new file mode 100644 index 0000000..edea52c --- /dev/null +++ b/config/com.bitcoinwolfe.node.plist @@ -0,0 +1,42 @@ + + + + + + Label + com.bitcoinwolfe.node + + + ProgramArguments + + __WOLFE_BINARY__ + start + + + + WorkingDirectory + __WOLFE_DIR__ + + + RunAtLoad + + + + KeepAlive + + SuccessfulExit + + + + + ThrottleInterval + 10 + + + StandardOutPath + __WOLFE_DIR__/data/wolfe-launchd.log + StandardErrorPath + __WOLFE_DIR__/data/wolfe-launchd.log + + diff --git a/crates/wolfe-node/src/main.rs b/crates/wolfe-node/src/main.rs index 48cfed8..8b2a8d7 100644 --- a/crates/wolfe-node/src/main.rs +++ b/crates/wolfe-node/src/main.rs @@ -425,6 +425,59 @@ async fn main() -> Result<()> { } }); info!("Lightning background processor started"); + + // Periodically (re)connect to persistent peers from config. + // Runs every 60 seconds: any persistent peer not currently connected + // is dialed. Hostnames are re-resolved on each tick so DHCP/DNS + // changes on the LAN don't permanently break reconnection. + if !config.lightning.persistent_peers.is_empty() { + let ln_reconnect = ln.clone(); + let ln_reconnect_shutdown = shutdown.clone(); + let peers = config.lightning.persistent_peers.clone(); + tokio::spawn(async move { + // Brief delay to let the listener bind first + tokio::time::sleep(std::time::Duration::from_secs(2)).await; + let mut interval = tokio::time::interval(std::time::Duration::from_secs(60)); + loop { + interval.tick().await; + if ln_reconnect_shutdown.load(Ordering::Relaxed) { + break; + } + + let connected: std::collections::HashSet = + ln_reconnect + .peer_manager() + .list_peers() + .into_iter() + .map(|p| p.counterparty_node_id) + .collect(); + + for peer_str in &peers { + match parse_ln_peer(peer_str) { + Ok((pubkey, addr)) => { + if connected.contains(&pubkey) { + continue; + } + match ln_reconnect.connect_peer(pubkey, addr).await { + Ok(()) => info!( + %pubkey, + %addr, + "persistent Lightning peer connected" + ), + Err(e) => debug!( + %peer_str, + ?e, + "persistent peer not reachable, will retry" + ), + } + } + Err(e) => warn!(%peer_str, %e, "invalid persistent_peers entry"), + } + } + } + }); + info!("persistent Lightning peer reconnector started"); + } } // ── Initialize RPC server ─────────────────────────────────────────── @@ -1027,3 +1080,17 @@ fn prune_block_files(blocks_dir: &std::path::Path, target_bytes: u64) -> Result< Ok(()) } + +/// Parse a "pubkey@host:port" string into a secp256k1 PublicKey and SocketAddr. +fn parse_ln_peer(s: &str) -> Result<(bitcoin::secp256k1::PublicKey, std::net::SocketAddr)> { + use std::net::ToSocketAddrs; + let (pk_str, addr_str) = s + .split_once('@') + .ok_or_else(|| anyhow::anyhow!("expected pubkey@host:port"))?; + let pubkey: bitcoin::secp256k1::PublicKey = pk_str.parse()?; + let addr = addr_str + .to_socket_addrs()? + .next() + .ok_or_else(|| anyhow::anyhow!("could not resolve {}", addr_str))?; + Ok((pubkey, addr)) +} diff --git a/crates/wolfe-types/src/config.rs b/crates/wolfe-types/src/config.rs index 8d26302..9418bb0 100644 --- a/crates/wolfe-types/src/config.rs +++ b/crates/wolfe-types/src/config.rs @@ -322,6 +322,8 @@ pub struct LightningConfig { pub max_channel_size_sat: u64, /// URL for Rapid Gossip Sync server (optional, speeds up initial gossip). pub rapid_gossip_sync_url: Option, + /// Peers to automatically reconnect to on startup (pubkey@host:port). + pub persistent_peers: Vec, } impl Default for LightningConfig { @@ -336,6 +338,7 @@ impl Default for LightningConfig { min_channel_size_sat: 20_000, max_channel_size_sat: 16_777_215, rapid_gossip_sync_url: None, + persistent_peers: vec![], } } } From 6e6e09c6778cb522f8e481f3d36498c5d8296423 Mon Sep 17 00:00:00 2001 From: Mike Rahel Date: Tue, 28 Apr 2026 21:31:43 -0400 Subject: [PATCH 2/4] Address PR review: SIGTERM handling, async DNS, plist + docs fixes - Handle SIGTERM alongside SIGINT/Ctrl+C so kill, systemctl stop, and launchctl unload all exit through the same graceful shutdown path. - Resolve persistent peer hostnames with tokio::net::lookup_host to keep blocking DNS off the runtime workers. - Validate persistent_peers pubkeys once at startup; invalid entries are logged once and skipped instead of warning every 60s. - Move the launchd plist log path out of data/ so a fresh install doesn't fail before the binary creates that directory. - README: document SIGTERM as a graceful stop, correct LaunchAgents to run on login (not boot) with a LaunchDaemons pointer, and add TimeoutStopSec=30 to the systemd example so Lightning state can persist. - Update the persistent_peers doc comment to reflect the ongoing reconnect behavior. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 13 ++- config/com.bitcoinwolfe.node.plist | 8 +- crates/wolfe-node/src/main.rs | 164 ++++++++++++++++++++--------- crates/wolfe-types/src/config.rs | 2 +- 4 files changed, 127 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index 44ed971..222e2b9 100644 --- a/README.md +++ b/README.md @@ -92,17 +92,17 @@ curl -s http://127.0.0.1:8332/ \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"stop"}' -# Via signal -kill $(pgrep -f "wolfe start") +# Via signal (SIGINT or SIGTERM) +kill -INT $(pgrep -f "wolfe start") # or: kill $(pgrep -f "wolfe start") ``` You can also stop from the **Settings** page in the web dashboard. -All methods trigger the same graceful shutdown: Lightning channel state is persisted, the consensus engine is interrupted cleanly, and peer connections are closed. +All methods trigger the same graceful shutdown: Lightning channel state is persisted, the consensus engine is interrupted cleanly, and peer connections are closed. Both `SIGINT` (Ctrl+C) and `SIGTERM` (default for `kill`, `systemctl stop`, and `launchctl unload`) are handled the same way. ### Auto-Start on macOS (launchd) -To have BitcoinWolfe start automatically on boot and restart after crashes: +To have BitcoinWolfe start automatically on login and restart after crashes: ```bash # 1. Copy the template plist @@ -133,6 +133,8 @@ launchctl load ~/Library/LaunchAgents/com.bitcoinwolfe.node.plist ``` > **Note:** The plist uses `KeepAlive` with `SuccessfulExit = false`, so launchd will restart the node if it crashes but not if you stop it intentionally via RPC `stop`, `Ctrl+C`, or the dashboard. +> +> `~/Library/LaunchAgents/` runs at user **login**, not at boot. For boot-time startup install the plist into `/Library/LaunchDaemons/` (requires `sudo`) and add a `UserName` key — note that `LaunchDaemons` run as root unless `UserName` is set. ### Auto-Start on Linux (systemd) @@ -151,6 +153,9 @@ WorkingDirectory=/path/to/BitcoinWolfe ExecStart=/path/to/BitcoinWolfe/target/release/wolfe start Restart=on-failure RestartSec=10 +# systemctl stop sends SIGTERM, which the node handles gracefully. +# Give Lightning state and consensus enough time to persist before SIGKILL. +TimeoutStopSec=30 [Install] WantedBy=multi-user.target diff --git a/config/com.bitcoinwolfe.node.plist b/config/com.bitcoinwolfe.node.plist index edea52c..83c69d5 100644 --- a/config/com.bitcoinwolfe.node.plist +++ b/config/com.bitcoinwolfe.node.plist @@ -33,10 +33,12 @@ ThrottleInterval 10 - + StandardOutPath - __WOLFE_DIR__/data/wolfe-launchd.log + __WOLFE_DIR__/wolfe-launchd.log StandardErrorPath - __WOLFE_DIR__/data/wolfe-launchd.log + __WOLFE_DIR__/wolfe-launchd.log diff --git a/crates/wolfe-node/src/main.rs b/crates/wolfe-node/src/main.rs index 8b2a8d7..6610a2f 100644 --- a/crates/wolfe-node/src/main.rs +++ b/crates/wolfe-node/src/main.rs @@ -427,56 +427,76 @@ async fn main() -> Result<()> { info!("Lightning background processor started"); // Periodically (re)connect to persistent peers from config. - // Runs every 60 seconds: any persistent peer not currently connected - // is dialed. Hostnames are re-resolved on each tick so DHCP/DNS - // changes on the LAN don't permanently break reconnection. + // Pubkeys are validated once here, then any persistent peer not + // currently connected is dialed every 60 seconds. Hostnames are + // re-resolved on each tick so DHCP/DNS changes on the LAN don't + // permanently break reconnection. if !config.lightning.persistent_peers.is_empty() { - let ln_reconnect = ln.clone(); - let ln_reconnect_shutdown = shutdown.clone(); - let peers = config.lightning.persistent_peers.clone(); - tokio::spawn(async move { - // Brief delay to let the listener bind first - tokio::time::sleep(std::time::Duration::from_secs(2)).await; - let mut interval = tokio::time::interval(std::time::Duration::from_secs(60)); - loop { - interval.tick().await; - if ln_reconnect_shutdown.load(Ordering::Relaxed) { - break; + let mut valid_peers: Vec<(bitcoin::secp256k1::PublicKey, String, String)> = Vec::new(); + for peer_str in &config.lightning.persistent_peers { + match split_ln_peer(peer_str) { + Ok((pubkey, addr_str)) => { + valid_peers.push((pubkey, addr_str, peer_str.clone())); } + Err(e) => { + warn!(%peer_str, %e, "invalid persistent_peers entry, skipping"); + } + } + } + + if !valid_peers.is_empty() { + let valid_count = valid_peers.len(); + let ln_reconnect = ln.clone(); + let ln_reconnect_shutdown = shutdown.clone(); + tokio::spawn(async move { + // Brief delay to let the listener bind first + tokio::time::sleep(std::time::Duration::from_secs(2)).await; + let mut interval = tokio::time::interval(std::time::Duration::from_secs(60)); + loop { + interval.tick().await; + if ln_reconnect_shutdown.load(Ordering::Relaxed) { + break; + } - let connected: std::collections::HashSet = - ln_reconnect - .peer_manager() - .list_peers() - .into_iter() - .map(|p| p.counterparty_node_id) - .collect(); - - for peer_str in &peers { - match parse_ln_peer(peer_str) { - Ok((pubkey, addr)) => { - if connected.contains(&pubkey) { + let connected: std::collections::HashSet = + ln_reconnect + .peer_manager() + .list_peers() + .into_iter() + .map(|p| p.counterparty_node_id) + .collect(); + + for (pubkey, addr_str, peer_str) in &valid_peers { + if connected.contains(pubkey) { + continue; + } + let addr = match resolve_addr(addr_str).await { + Ok(a) => a, + Err(e) => { + debug!(%peer_str, ?e, "persistent peer DNS resolve failed, will retry"); continue; } - match ln_reconnect.connect_peer(pubkey, addr).await { - Ok(()) => info!( - %pubkey, - %addr, - "persistent Lightning peer connected" - ), - Err(e) => debug!( - %peer_str, - ?e, - "persistent peer not reachable, will retry" - ), - } + }; + match ln_reconnect.connect_peer(*pubkey, addr).await { + Ok(()) => info!( + %pubkey, + %addr, + "persistent Lightning peer connected" + ), + Err(e) => debug!( + %peer_str, + ?e, + "persistent peer not reachable, will retry" + ), } - Err(e) => warn!(%peer_str, %e, "invalid persistent_peers entry"), } } - } - }); - info!("persistent Lightning peer reconnector started"); + }); + info!( + valid = valid_count, + "persistent Lightning peer reconnector started" + ); + } } } @@ -952,9 +972,8 @@ async fn main() -> Result<()> { } } - // Shutdown on Ctrl+C - _ = tokio::signal::ctrl_c() => { - info!("shutdown signal received"); + // Shutdown on SIGINT (Ctrl+C) or SIGTERM (kill, systemctl stop, launchctl unload) + _ = shutdown_signal() => { shutdown.store(true, Ordering::Relaxed); break; } @@ -1081,16 +1100,57 @@ fn prune_block_files(blocks_dir: &std::path::Path, target_bytes: u64) -> Result< Ok(()) } -/// Parse a "pubkey@host:port" string into a secp256k1 PublicKey and SocketAddr. -fn parse_ln_peer(s: &str) -> Result<(bitcoin::secp256k1::PublicKey, std::net::SocketAddr)> { - use std::net::ToSocketAddrs; +/// Parse a "pubkey@host:port" string. Validates the pubkey synchronously and +/// returns the address portion as a string for later async resolution. Splitting +/// pubkey parsing from DNS lookup lets us validate the static parts once at +/// startup while still re-resolving hostnames each reconnect cycle. +fn split_ln_peer(s: &str) -> Result<(bitcoin::secp256k1::PublicKey, String)> { let (pk_str, addr_str) = s .split_once('@') .ok_or_else(|| anyhow::anyhow!("expected pubkey@host:port"))?; let pubkey: bitcoin::secp256k1::PublicKey = pk_str.parse()?; - let addr = addr_str - .to_socket_addrs()? + Ok((pubkey, addr_str.to_string())) +} + +/// Resolve a "host:port" string to a SocketAddr without blocking the runtime. +async fn resolve_addr(addr_str: &str) -> Result { + tokio::net::lookup_host(addr_str) + .await? .next() - .ok_or_else(|| anyhow::anyhow!("could not resolve {}", addr_str))?; - Ok((pubkey, addr)) + .ok_or_else(|| anyhow::anyhow!("could not resolve {}", addr_str)) +} + +/// Wait for any termination signal. On Unix this is SIGINT or SIGTERM, so +/// `Ctrl+C`, `kill`, `systemctl stop`, and `launchctl unload` all trigger the +/// same graceful shutdown path. +async fn shutdown_signal() { + #[cfg(unix)] + { + use tokio::signal::unix::{signal, SignalKind}; + let mut sigterm = match signal(SignalKind::terminate()) { + Ok(s) => s, + Err(e) => { + error!(?e, "failed to install SIGTERM handler, falling back to Ctrl+C only"); + let _ = tokio::signal::ctrl_c().await; + return; + } + }; + let mut sigint = match signal(SignalKind::interrupt()) { + Ok(s) => s, + Err(e) => { + error!(?e, "failed to install SIGINT handler, falling back to Ctrl+C only"); + let _ = tokio::signal::ctrl_c().await; + return; + } + }; + tokio::select! { + _ = sigterm.recv() => info!("SIGTERM received"), + _ = sigint.recv() => info!("SIGINT received"), + } + } + #[cfg(not(unix))] + { + let _ = tokio::signal::ctrl_c().await; + info!("Ctrl+C received"); + } } diff --git a/crates/wolfe-types/src/config.rs b/crates/wolfe-types/src/config.rs index 9418bb0..4a7a5a9 100644 --- a/crates/wolfe-types/src/config.rs +++ b/crates/wolfe-types/src/config.rs @@ -322,7 +322,7 @@ pub struct LightningConfig { pub max_channel_size_sat: u64, /// URL for Rapid Gossip Sync server (optional, speeds up initial gossip). pub rapid_gossip_sync_url: Option, - /// Peers to automatically reconnect to on startup (pubkey@host:port). + /// Peers to keep connected to, periodically reconnecting as needed (pubkey@host:port). pub persistent_peers: Vec, } From 07f5b0ea7fc7fb49085527d1458e305ab09837cb Mon Sep 17 00:00:00 2001 From: Mike Rahel Date: Tue, 28 Apr 2026 21:35:24 -0400 Subject: [PATCH 3/4] Add optional nostr profile picture URL Threads a `picture` field through NostrConfig and the NostrBridge so it shows up in the published kind-0 metadata event. Invalid URLs are warned once and skipped rather than failing the whole metadata publish. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/wolfe-node/src/main.rs | 1 + crates/wolfe-nostr/src/lib.rs | 14 +++++++++++++- crates/wolfe-types/src/config.rs | 3 +++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/crates/wolfe-node/src/main.rs b/crates/wolfe-node/src/main.rs index 6610a2f..78d334b 100644 --- a/crates/wolfe-node/src/main.rs +++ b/crates/wolfe-node/src/main.rs @@ -557,6 +557,7 @@ async fn main() -> Result<()> { config.nostr.fee_oracle_interval_secs, config.nostr.name.clone(), config.nostr.about.clone(), + config.nostr.picture.clone(), ) .await { diff --git a/crates/wolfe-nostr/src/lib.rs b/crates/wolfe-nostr/src/lib.rs index 138a716..7b0246a 100644 --- a/crates/wolfe-nostr/src/lib.rs +++ b/crates/wolfe-nostr/src/lib.rs @@ -47,6 +47,7 @@ pub struct NostrBridge { keys: Keys, profile_name: Option, profile_about: Option, + profile_picture: Option, } /// Handle for sending events to the Nostr bridge from the main loop. @@ -80,6 +81,7 @@ impl NostrBridge { fee_oracle_interval_secs: u64, profile_name: Option, profile_about: Option, + profile_picture: Option, ) -> Result<(Self, NostrSender, Arc), NostrError> { let keys = match secret_key { Some(sk) => Keys::parse(sk).map_err(|e| NostrError::InvalidKey(e.to_string()))?, @@ -115,6 +117,7 @@ impl NostrBridge { keys, profile_name, profile_about, + profile_picture, }, NostrSender { tx }, shared_client, @@ -141,7 +144,10 @@ impl NostrBridge { ); // Publish profile metadata (NIP-01 kind 0) if configured - if self.profile_name.is_some() || self.profile_about.is_some() { + if self.profile_name.is_some() + || self.profile_about.is_some() + || self.profile_picture.is_some() + { let mut metadata = Metadata::new(); if let Some(ref name) = self.profile_name { metadata = metadata.name(name); @@ -149,6 +155,12 @@ impl NostrBridge { if let Some(ref about) = self.profile_about { metadata = metadata.about(about); } + if let Some(ref picture) = self.profile_picture { + match Url::parse(picture) { + Ok(url) => metadata = metadata.picture(url), + Err(e) => warn!(picture, ?e, "invalid nostr.picture URL, skipping"), + } + } match self.client.set_metadata(&metadata).await { Ok(output) => { info!( diff --git a/crates/wolfe-types/src/config.rs b/crates/wolfe-types/src/config.rs index 4a7a5a9..798e2e2 100644 --- a/crates/wolfe-types/src/config.rs +++ b/crates/wolfe-types/src/config.rs @@ -286,6 +286,8 @@ pub struct NostrConfig { pub name: Option, /// Profile about/bio text. pub about: Option, + /// Profile picture URL (NIP-01 kind 0 metadata). + pub picture: Option, /// Relay URLs to publish events to. pub relays: Vec, /// Publish new block announcements to relays. @@ -377,6 +379,7 @@ impl Default for NostrConfig { secret_key: None, name: None, about: None, + picture: None, relays: vec![ "wss://relay.damus.io".to_string(), "wss://nos.lol".to_string(), From bd56166447e6fd589a834a1b09844b42294a0c57 Mon Sep 17 00:00:00 2001 From: Mike Rahel Date: Tue, 28 Apr 2026 21:37:34 -0400 Subject: [PATCH 4/4] Fix CI: rustfmt and clippy too_many_arguments cargo fmt --check on the prior commits flagged two long error!() calls and clippy was hitting too_many_arguments after the picture field was added to NostrBridge::new (8 args, default threshold is 7). Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/wolfe-node/src/main.rs | 10 ++++++++-- crates/wolfe-nostr/src/lib.rs | 1 + 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/crates/wolfe-node/src/main.rs b/crates/wolfe-node/src/main.rs index 78d334b..699ec32 100644 --- a/crates/wolfe-node/src/main.rs +++ b/crates/wolfe-node/src/main.rs @@ -1131,7 +1131,10 @@ async fn shutdown_signal() { let mut sigterm = match signal(SignalKind::terminate()) { Ok(s) => s, Err(e) => { - error!(?e, "failed to install SIGTERM handler, falling back to Ctrl+C only"); + error!( + ?e, + "failed to install SIGTERM handler, falling back to Ctrl+C only" + ); let _ = tokio::signal::ctrl_c().await; return; } @@ -1139,7 +1142,10 @@ async fn shutdown_signal() { let mut sigint = match signal(SignalKind::interrupt()) { Ok(s) => s, Err(e) => { - error!(?e, "failed to install SIGINT handler, falling back to Ctrl+C only"); + error!( + ?e, + "failed to install SIGINT handler, falling back to Ctrl+C only" + ); let _ = tokio::signal::ctrl_c().await; return; } diff --git a/crates/wolfe-nostr/src/lib.rs b/crates/wolfe-nostr/src/lib.rs index 7b0246a..d56265c 100644 --- a/crates/wolfe-nostr/src/lib.rs +++ b/crates/wolfe-nostr/src/lib.rs @@ -73,6 +73,7 @@ impl NostrBridge { /// /// Returns `(bridge, sender, client)` where `client` is a shared handle /// that can be used by RPC handlers to publish events or query relays. + #[allow(clippy::too_many_arguments)] pub async fn new( secret_key: Option<&str>, relays: &[String],