1
0
forked from GRIN/grim

Build 71: clearnet price + update check, Pay-screen UI polish

Following upstream Grim's posture for non-sensitive metadata: the fiat price
preview and the update check now go direct over HTTPS instead of the mixnet, so
the price shows promptly without waiting on the mixnet bootstrap. Payments,
relays and identity stay mixnet-only.

- price.rs: fetch the rate over clearnet (plain reqwest), no proxy.
- update check: on by default like Grim, repointed at Goblin's own GitHub
  releases, build-number aware (is_update compares buildNN), Goblin asset names,
  GitHub User-Agent header.
- Pay screen: status-bar icons go dark on the bright yellow surface
  (status_bar_white_icons honours a per-frame yellow-surface flag); the hero
  amount shows the goblin mark as its unit in place of the ツ glyph.
This commit is contained in:
2ro
2026-06-14 14:21:39 -04:00
parent 851ae1c565
commit 726e96130c
6 changed files with 105 additions and 65 deletions
+14 -1
View File
@@ -224,9 +224,22 @@ pub fn tokens() -> &'static ThemeTokens {
}
}
/// Set each frame by the Pay surface (which paints a bright yellow top under a
/// possibly-dark global theme), so the status bar can pick readable icons for it.
static YELLOW_SURFACE: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
/// Flag whether the bright Pay/yellow surface is currently on screen.
pub fn set_status_surface_yellow(yellow: bool) {
YELLOW_SURFACE.store(yellow, std::sync::atomic::Ordering::Relaxed);
}
/// Whether the status bar should use light (white) icons: true on the dark
/// theme (dark top), false on the light/yellow themes (bright top).
/// theme (dark top), false on the light/yellow themes (bright top). The bright
/// Pay surface forces dark icons even when the global theme is dark.
pub fn status_bar_white_icons() -> bool {
if YELLOW_SURFACE.load(std::sync::atomic::Ordering::Relaxed) {
return false;
}
tokens().dark_base
}
+5 -3
View File
@@ -380,6 +380,8 @@ impl GoblinWalletView {
// widget inside pick up the yellow tokens together.
let pay = self.tab == Tab::Pay;
let _pay_theme = pay.then(|| theme::scoped(theme::ThemeKind::Yellow));
// Bright yellow top → dark status-bar icons (see status_bar_white_icons).
theme::set_status_surface_yellow(pay);
let panel_fill = if pay { theme::YELLOW.bg } else { t.bg };
egui::CentralPanel::default()
.frame(egui::Frame {
@@ -946,10 +948,10 @@ impl GoblinWalletView {
let dx = 14.0 * (1.0 - p) * (p * std::f32::consts::PI * 9.0).sin();
// Red flash that eases back to the normal ink over the shake.
let num = lerp_color(t.neg, t.text, p);
let mark = lerp_color(t.neg, t.text_dim, p);
w::amount_text_centered_shifted(ui, &display, 76.0, num, mark, dx);
// goblin mark keeps its colours; only the digits flash red.
w::amount_text_centered_goblin(ui, &display, 76.0, num, dx);
} else {
w::amount_text_centered(ui, &display, 76.0);
w::amount_text_centered_goblin(ui, &display, 76.0, t.text, 0.0);
}
if let Ok(grin) = display.parse::<f64>() {
if let Some(preview) = pairing_preview(grin) {
+36
View File
@@ -227,6 +227,42 @@ pub fn amount_text_centered_shifted(
});
}
/// Pay-screen hero amount: the number with the **goblin mark** as its unit (in
/// place of the ツ glyph), centered. `dx` is the over-balance shake offset.
pub fn amount_text_centered_goblin(ui: &mut Ui, value: &str, size: f32, num_ink: Color32, dx: f32) {
let avail = ui.available_width();
let measure = |ui: &Ui, sz: f32| -> f32 {
ui.painter()
.layout_no_wrap(value.to_string(), FontId::new(sz, fonts::bold()), num_ink)
.size()
.x
};
// Goblin mark ~half the digit height, with a small gap before it.
let mark_of = |sz: f32| sz * 0.52;
let gap = size * 0.06;
let mut size = size;
let total0 = measure(ui, size) + gap + mark_of(size);
if total0 > avail && total0 > 1.0 {
size = (size * (avail / total0) * 0.97).clamp(14.0, size);
}
let mark = mark_of(size);
let total = measure(ui, size) + gap + mark;
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 0.0;
ui.add_space(((ui.available_width() - total) / 2.0 + dx).max(0.0));
ui.label(
RichText::new(value)
.font(FontId::new(size, fonts::bold()))
.color(num_ink),
);
ui.add_space(gap);
ui.add(
egui::Image::new(egui::include_image!("../../../../img/goblin-logo2.svg"))
.fit_to_exact_size(Vec2::splat(mark)),
);
});
}
/// An uppercase letterspaced kicker label.
pub fn kicker(ui: &mut Ui, text: &str) {
let t = theme::tokens();
+16 -11
View File
@@ -12,20 +12,21 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//! GRIN price preview, fetched over the Nym mixnet and cached per currency.
//! GRIN price preview, cached per currency. The rate is non-sensitive metadata
//! (no payment info), so — like the update check, and like upstream Grim — it is
//! fetched direct over HTTPS rather than waiting on the mixnet, keeping the
//! preview prompt. Payments, relays and identity stay mixnet-only.
use lazy_static::lazy_static;
use parking_lot::RwLock;
use std::collections::{HashMap, HashSet};
use std::time::{SystemTime, UNIX_EPOCH};
use crate::nym;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
/// Cache refresh interval (seconds).
const REFRESH_SECS: i64 = 300;
/// Minimum delay between fetch attempts for a currency, so a failing fetch
/// (e.g. the mixnet still bootstrapping) does not respawn a thread every frame.
/// (e.g. no network) does not respawn a thread every frame.
const RETRY_SECS: i64 = 30;
lazy_static! {
@@ -59,7 +60,7 @@ pub fn grin_rate(vs: &str) -> Option<f64> {
cached.map(|(rate, _)| rate)
}
/// Spawn a background refresh over the mixnet for one currency (deduped per code).
/// Spawn a background refresh for one currency (deduped per code).
fn trigger_refresh(vs: String) {
let t = now();
{
@@ -90,22 +91,26 @@ fn trigger_refresh(vs: String) {
});
}
/// Fetch the GRIN/`vs` rate from CoinGecko over the Nym mixnet.
/// Fetch the GRIN/`vs` rate from CoinGecko, direct over HTTPS.
async fn fetch_rate(vs: &str) -> Option<f64> {
let url = format!(
"https://api.coingecko.com/api/v3/simple/price?ids=grin&vs_currencies={}",
vs
);
// CoinGecko rejects requests without a User-Agent (403). A static,
// non-identifying UA is fine over the mixnet.
let headers = vec![("User-Agent".to_string(), "goblin-wallet".to_string())];
let body = nym::http_request("GET", url, None, headers).await?;
// non-identifying UA.
let client = reqwest::Client::builder()
.user_agent("goblin-wallet")
.timeout(Duration::from_secs(20))
.build()
.ok()?;
let body = client.get(&url).send().await.ok()?.text().await.ok()?;
let parsed: Option<f64> = serde_json::from_str::<serde_json::Value>(&body)
.ok()
.and_then(|doc| doc.get("grin")?.get(vs)?.as_f64());
if parsed.is_none() {
log::warn!(
"price: unexpected response from rate API (mixnet exit blocked?): {}",
"price: unexpected response from rate API: {}",
body.chars().take(120).collect::<String>()
);
}
+29 -44
View File
@@ -47,42 +47,43 @@ const ARM_ARCH: &str = "arm";
const ARCH: &'static str = ARM_ARCH;
/// Base endpoint to download the release.
const BASE_DOWNLOAD_URL: &'static str = "https://code.gri.mw/GUI/grim/releases/download/";
const BASE_DOWNLOAD_URL: &'static str = "https://github.com/2ro/goblin/releases/download/";
impl ReleaseInfo {
/// Get version number.
/// Release version (the build tag, e.g. "build71").
pub fn version(&self) -> String {
self.tag_name.replace("v", "")
self.tag_name.clone()
}
/// Get artifact release name based on current platform.
/// Get artifact release name based on current platform. Matches the assets
/// attached to Goblin's GitHub releases; platforms Goblin doesn't ship
/// (linux-arm, macOS, windows-arm) return None.
fn name(&self) -> Option<String> {
let os = OperatingSystem::from_target_os();
match os {
OperatingSystem::Unknown => None,
OperatingSystem::Android => {
let name = if ARCH == ARM_ARCH {
format!("grim-{}-android.apk", self.tag_name)
format!("goblin-{}-android-arm.apk", self.tag_name)
} else {
format!("grim-{}-android-x86_64.apk", self.tag_name)
format!("goblin-{}-android-x86_64.apk", self.tag_name)
};
Some(name)
}
OperatingSystem::IOS => None,
OperatingSystem::Nix => {
let name = if ARCH == ARM_ARCH {
format!("grim-{}-linux-arm.AppImage", self.tag_name)
if ARCH == ARM_ARCH {
None
} else {
format!("grim-{}-linux-x86_64.AppImage", self.tag_name)
};
Some(name)
Some(format!("goblin-{}-linux-x86_64.AppImage", self.tag_name))
}
}
OperatingSystem::Mac => Some(format!("grim-{}-macos-universal.zip", self.tag_name)),
OperatingSystem::Mac => None,
OperatingSystem::Windows => {
if ARCH == ARM_ARCH {
None
} else {
Some(format!("grim-{}-win-x86_64.msi", self.tag_name))
Some(format!("goblin-{}-win-x86_64.zip", self.tag_name))
}
}
}
@@ -119,46 +120,30 @@ impl ReleaseInfo {
None
}
/// Check if release is update.
/// Whether this release is newer than the running build. Goblin versions by
/// build number ("buildNN" tags) rather than semver, so compare the numbers.
pub fn is_update(&self) -> bool {
let cur = crate::VERSION;
let ver = self.version();
if cur == ver {
return false;
}
let cur_numbers: Vec<i32> = cur
.split(".")
.filter_map(|s| s.parse::<i32>().ok())
.collect();
let ver_numbers: Vec<i32> = ver
.split(".")
.filter_map(|s| s.parse::<i32>().ok())
.collect();
if cur_numbers.len() != ver_numbers.len() {
return true;
}
for (i, num) in ver_numbers.iter().enumerate() {
if num > &cur_numbers.get(i).unwrap() {
if i == 0 {
return true;
} else if i == 1 && cur_numbers.get(0).unwrap() == ver_numbers.get(0).unwrap() {
return true;
} else if i == 2 && cur_numbers.get(1).unwrap() == ver_numbers.get(1).unwrap() {
return true;
}
}
}
false
let cur: u64 = crate::BUILD.trim().parse().unwrap_or(0);
let rel: u64 = self
.tag_name
.trim()
.trim_start_matches("build")
.parse()
.unwrap_or(0);
rel > cur
}
}
/// API endpoint to check last release.
const REQUEST_URL: &'static str = "https://code.gri.mw/api/v1/repos/gui/grim/releases/latest";
/// API endpoint to check last release (Goblin's own GitHub releases).
const REQUEST_URL: &'static str = "https://api.github.com/repos/2ro/goblin/releases/latest";
pub async fn retrieve_release() -> Result<ReleaseInfo, String> {
let req = hyper::Request::builder()
.method(hyper::Method::GET)
.uri(REQUEST_URL)
// GitHub's API rejects requests without a User-Agent.
.header("User-Agent", "goblin-wallet")
.header("Accept", "application/vnd.github+json")
.body(Empty::<Bytes>::new())
.unwrap();
if let Ok(resp) = HttpClient::send(req).await {
+5 -6
View File
@@ -198,12 +198,11 @@ impl Default for AppConfig {
use_socks_proxy: None,
http_proxy_url: None,
socks_proxy_url: None,
// Off by default: the legacy update check hits code.gri.mw (GRIM's
// gitea) directly over CLEARNET via the old HttpClient — it leaks
// "this user runs Goblin" metadata (defeating the nothing-clearnet
// mixnet model) and points at the wrong project's releases. Opt-in
// only until reworked to run over the mixnet against Goblin releases.
check_updates: Some(false),
// On by default, like upstream Grim: checks Goblin's own GitHub
// releases direct over HTTPS (see http/release.rs). This is the same
// non-sensitive-metadata-over-clearnet posture Grim uses for its
// update check — payments, relays and identity still stay mixnet-only.
check_updates: Some(true),
app_update: None,
}
}