diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
index 1148091..46c204a 100644
Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
index 1ad2e38..cd4c37b 100644
Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
index 1148091..b39b815 100644
Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
index e533f41..aaa1b38 100644
Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
index cc56f3e..f689664 100644
Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
index dd17d2b..b90451d 100644
Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
index 4f02e6e..84a426e 100644
Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
index c22df52..be8e877 100644
Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
index 4f02e6e..470fa70 100644
Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
index ecf792e..92caf1c 100644
Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
index 1332a8e..3e9dca7 100644
Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
index 627fe7d..ef7f40e 100644
Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
index e0a58c9..3db912b 100644
Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
index 151e261..6ff7500 100644
Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
index e0a58c9..9caa7bc 100644
Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/img/goblin-logo2.svg b/img/goblin-logo2.svg
new file mode 100644
index 0000000..0fd9751
--- /dev/null
+++ b/img/goblin-logo2.svg
@@ -0,0 +1,48 @@
+
+
+
diff --git a/img/icon.png b/img/icon.png
index 7753cf2..f5b1440 100644
Binary files a/img/icon.png and b/img/icon.png differ
diff --git a/linux/Goblin.AppDir/goblin.png b/linux/Goblin.AppDir/goblin.png
new file mode 100644
index 0000000..f5b1440
Binary files /dev/null and b/linux/Goblin.AppDir/goblin.png differ
diff --git a/linux/Goblin.AppDir/grim.png b/linux/Goblin.AppDir/grim.png
deleted file mode 100644
index b176447..0000000
Binary files a/linux/Goblin.AppDir/grim.png and /dev/null differ
diff --git a/src/gui/app.rs b/src/gui/app.rs
index addb690..fa9b414 100755
--- a/src/gui/app.rs
+++ b/src/gui/app.rs
@@ -167,12 +167,28 @@ impl App {
/// Draw custom desktop window frame content.
fn custom_frame_ui(&mut self, ui: &mut egui::Ui, is_fullscreen: bool) {
- // Paint the whole window with the theme background first: the frame
- // margin ring is otherwise transparent, which X11 without a
- // compositor renders as a black border (and a black strip under the
- // sidebar in light/yellow themes).
+ // Paint the window area inside the frame margin with the theme
+ // background first: surface gaps are otherwise transparent, which X11
+ // without a compositor renders as black (strip under the sidebar in
+ // light/yellow themes). The margin ring itself must STAY transparent —
+ // painting it gives the window a visible border under a compositor.
+ let fill_rect = if is_fullscreen {
+ ui.max_rect()
+ } else {
+ ui.max_rect().shrink(Content::WINDOW_FRAME_MARGIN)
+ };
+ let fill_rounding = if is_fullscreen {
+ CornerRadius::ZERO
+ } else {
+ CornerRadius {
+ nw: 8,
+ ne: 8,
+ sw: 0,
+ se: 0,
+ }
+ };
ui.painter()
- .rect_filled(ui.max_rect(), CornerRadius::ZERO, Colors::fill());
+ .rect_filled(fill_rect, fill_rounding, Colors::fill());
let content_bg_rect = {
let mut r = ui.max_rect();
if !is_fullscreen {
diff --git a/src/gui/views/goblin/mod.rs b/src/gui/views/goblin/mod.rs
index ea4fef3..9bdc26a 100644
--- a/src/gui/views/goblin/mod.rs
+++ b/src/gui/views/goblin/mod.rs
@@ -2688,7 +2688,7 @@ pub fn widgets_logo(ui: &mut egui::Ui) {
/// Tinted goblin mark at a given size.
pub fn widgets_logo_sized(ui: &mut egui::Ui, size: f32) {
let (rect, _) = ui.allocate_exact_size(Vec2::splat(size), Sense::hover());
- let img = egui::Image::new(egui::include_image!("../../../../img/goblin-logo2-256.png"))
+ let img = egui::Image::new(egui::include_image!("../../../../img/goblin-logo2.svg"))
.tint(theme::tokens().text)
.fit_to_exact_size(Vec2::splat(size));
img.paint_at(ui, rect);
diff --git a/src/gui/views/goblin/widgets.rs b/src/gui/views/goblin/widgets.rs
index 3d743ad..679827d 100644
--- a/src/gui/views/goblin/widgets.rs
+++ b/src/gui/views/goblin/widgets.rs
@@ -316,7 +316,7 @@ pub fn qr_code(ui: &mut Ui, text: &str, size: f32) {
ui.painter()
.rect_filled(b_rect, CornerRadius::same((backing * 0.18) as u8), plate);
let m_rect = egui::Rect::from_center_size(rect.center(), Vec2::splat(backing * 0.72));
- egui::Image::new(egui::include_image!("../../../../img/goblin-logo2-256.png"))
+ egui::Image::new(egui::include_image!("../../../../img/goblin-logo2.svg"))
.tint(ink)
.fit_to_exact_size(m_rect.size())
.paint_at(ui, m_rect);
diff --git a/src/gui/views/views.rs b/src/gui/views/views.rs
index 31b4217..4470c9e 100644
--- a/src/gui/views/views.rs
+++ b/src/gui/views/views.rs
@@ -694,7 +694,7 @@ impl View {
pub fn app_logo_name_version(ui: &mut egui::Ui) {
ui.add_space(-1.0);
// Goblin mark (white master, tinted for the current theme).
- let logo = egui::include_image!("../../../img/goblin-logo2-256.png");
+ let logo = egui::include_image!("../../../img/goblin-logo2.svg");
// Show application logo and name.
ui.scope(|ui| {
ui.set_opacity(0.9);
diff --git a/src/lib.rs b/src/lib.rs
index 10d843b..ceed9b9 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -117,6 +117,8 @@ pub fn start(options: NativeOptions, app_creator: eframe::AppCreator) -> eframe:
if AppConfig::autostart_node() {
Node::start();
}
+ // Pre-warm the Tor client so price/NIP-05/nostr are ready at first use.
+ tor::Tor::warm_up();
// Launch graphical interface.
eframe::run_native("Goblin", options, app_creator)
}
diff --git a/src/main.rs b/src/main.rs
index f0a2e9f..8a725ff 100755
--- a/src/main.rs
+++ b/src/main.rs
@@ -87,6 +87,9 @@ fn start_desktop_gui(platform: grim::gui::platform::Desktop) {
let is_mac = os == egui::os::OperatingSystem::Mac;
let is_win = os == egui::os::OperatingSystem::Windows;
viewport = viewport
+ // Wayland taskbars resolve the icon through the .desktop file whose
+ // name matches this app id (see linux/Goblin.AppDir/goblin.desktop).
+ .with_app_id("goblin")
.with_fullsize_content_view(true)
.with_window_level(egui::WindowLevel::Normal)
.with_title_shown(is_win)
diff --git a/src/tor/tor.rs b/src/tor/tor.rs
index c380023..7bc17e6 100644
--- a/src/tor/tor.rs
+++ b/src/tor/tor.rs
@@ -27,7 +27,7 @@ use parking_lot::RwLock;
use safelog::DisplayRedacted;
use sha2::Sha512;
use std::collections::{BTreeMap, BTreeSet};
-use std::net::{IpAddr, Ipv4Addr, SocketAddr};
+use std::net::{IpAddr, Ipv4Addr, SocketAddr, TcpStream, ToSocketAddrs};
use std::str::FromStr;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
@@ -172,6 +172,81 @@ impl Tor {
(config, bridges, max_two_bridges)
}
+ /// Endpoint (host, port) a bridge dials first, for a quick reachability probe.
+ /// Webtunnel and obfs4 need a working TCP path to this endpoint before anything
+ /// else can happen; snowflake is CDN-fronted, so it gets no probe (None).
+ fn bridge_probe_addr(bridge: &TorBridge) -> Option<(String, u16)> {
+ let line = bridge.connection_line();
+ match bridge {
+ TorBridge::Webtunnel(_, _) => {
+ let url = line
+ .split_whitespace()
+ .find_map(|t| t.strip_prefix("url="))?;
+ let rest = url
+ .strip_prefix("https://")
+ .or_else(|| url.strip_prefix("http://"))?;
+ let host_port = rest.split('/').next()?;
+ let mut parts = host_port.splitn(2, ':');
+ let host = parts.next()?.to_string();
+ let port = parts.next().and_then(|p| p.parse().ok()).unwrap_or(443);
+ Some((host, port))
+ }
+ TorBridge::Obfs4(_, _) => {
+ let addr = line.split_whitespace().nth(1)?;
+ let mut parts = addr.rsplitn(2, ':');
+ let port = parts.next()?.parse().ok()?;
+ let host = parts
+ .next()?
+ .trim_start_matches('[')
+ .trim_end_matches(']')
+ .to_string();
+ Some((host, port))
+ }
+ TorBridge::Snowflake(_, _) => None,
+ }
+ }
+
+ /// Order bridges live-endpoint-first via a parallel TCP probe. A dead bridge
+ /// otherwise costs a full bootstrap timeout (60s) per attempt, which reads as
+ /// the wallet being stuck on "connecting…" while the list is ground through.
+ fn sort_bridges_by_reachability(bridges: Vec) -> Vec {
+ if bridges.len() < 2 {
+ return bridges;
+ }
+ let probes: Vec<(TorBridge, thread::JoinHandle)> = bridges
+ .into_iter()
+ .map(|b| {
+ let addr = Self::bridge_probe_addr(&b);
+ let handle = thread::spawn(move || match addr {
+ // No probeable endpoint: assume reachable.
+ None => true,
+ Some((host, port)) => (host.as_str(), port)
+ .to_socket_addrs()
+ .ok()
+ .and_then(|mut addrs| addrs.next())
+ .map(|a| {
+ TcpStream::connect_timeout(&a, Duration::from_millis(4000)).is_ok()
+ })
+ .unwrap_or(false),
+ });
+ (b, handle)
+ })
+ .collect();
+ let mut alive = vec![];
+ let mut dead = vec![];
+ for (b, handle) in probes {
+ if handle.join().unwrap_or(true) {
+ alive.push(b);
+ } else {
+ dead.push(b);
+ }
+ }
+ // Probes can lie on hostile networks — keep dead ones as a last resort
+ // rather than dropping them.
+ alive.extend(dead);
+ alive
+ }
+
/// Build bootstrapped client from provided config.
fn build_client_bootstrap(config: TorClientConfig) -> Option> {
let client_res = TorClient::with_runtime(TOR_STATE.runtime.clone())
@@ -190,8 +265,10 @@ impl Tor {
.runtime()
.spawn(async move {
let task = c.bootstrap();
- // Bootstrap client with 60s timeout.
- if tokio::time::timeout(Duration::from_millis(60000), task)
+ // Bootstrap client with 120s timeout: a cold-cache directory
+ // consensus over a webtunnel bridge regularly needs more than
+ // 60s, and timing it out throws the progress away.
+ if tokio::time::timeout(Duration::from_millis(120000), task)
.await
.is_ok()
{
@@ -222,10 +299,11 @@ impl Tor {
return;
}
}
- // Cleanup keys, state and cache.
+ // Cleanup onion service keys only. State and cache are KEPT: the cache
+ // holds the directory consensus, and wiping it forced a full re-download
+ // over the (slow) bridge on every app start — minutes of "connecting…"
+ // where a warm cache bootstraps in seconds.
fs::remove_dir_all(TorConfig::keystore_path()).unwrap_or_default();
- fs::remove_dir_all(TorConfig::state_path()).unwrap_or_default();
- fs::remove_dir_all(TorConfig::cache_path()).unwrap_or_default();
TOR_STATE.client_launching.store(true, Ordering::Relaxed);
// Get initial bridges.
let initial_bridges = if let Some(b) = TorConfig::get_bridge() {
@@ -239,7 +317,7 @@ impl Tor {
bridge
})
.collect::>();
- Some(bridges)
+ Some(Self::sort_bridges_by_reachability(bridges))
} else {
None
};
@@ -271,9 +349,8 @@ impl Tor {
TOR_STATE.client_config.write().replace((c, config.clone()));
break;
} else {
- // Cleanup state and cache.
- fs::remove_dir_all(TorConfig::state_path()).unwrap_or_default();
- fs::remove_dir_all(TorConfig::cache_path()).unwrap_or_default();
+ // Keep state and cache between attempts so directory consensus
+ // progress accumulates instead of restarting from zero.
// Check unused bridges to check another part.
if !unused_bridges.is_empty() {
config_bridges = Self::build_config(Some(unused_bridges));
@@ -286,7 +363,8 @@ impl Tor {
.iter()
.map(|b| TorBridge::Webtunnel(TorConfig::webtunnel_path(), b.to_string()))
.collect::>();
- config_bridges = Self::build_config(Some(add_bridges));
+ config_bridges =
+ Self::build_config(Some(Self::sort_bridges_by_reachability(add_bridges)));
continue;
} else if TorConfig::get_bridge().is_some() {
// Launch without bridges if all attempts failed.
@@ -388,6 +466,15 @@ impl Tor {
r_client_config.clone()
}
+ /// Pre-warm the Tor client in the background so it is already bootstrapped
+ /// by the time the first consumer (price feed, NIP-05, nostr relays) needs
+ /// it — otherwise nothing launches it until a wallet finishes opening.
+ pub fn warm_up() {
+ thread::spawn(|| {
+ let _ = Self::isolated_client_blocking();
+ });
+ }
+
/// Get an isolated Tor client for outgoing connections (e.g. Nostr relays),
/// launching the embedded client first when needed. Blocking: bootstrap can
/// take up to a minute, call from a blocking-friendly context.
@@ -550,10 +637,9 @@ impl Tor {
let mut w_services = TOR_STATE.start.write();
*w_services = services.clone();
}
- // Cleanup keys, state and cache.
+ // Cleanup onion service keys (services re-save them on relaunch). State
+ // and cache are kept so the restart reuses the directory consensus.
fs::remove_dir_all(TorConfig::keystore_path()).unwrap_or_default();
- fs::remove_dir_all(TorConfig::state_path()).unwrap_or_default();
- fs::remove_dir_all(TorConfig::cache_path()).unwrap_or_default();
{
let mut w_client = TOR_STATE.client_config.write();
*w_client = None;