mirror of
https://code.gri.mw/GUI/grim.git
synced 2026-07-16 03:38:55 +00:00
Build 19: open-wallet shortcuts, Pay QR scan, configurable pairing, hide yellow
- Lower-left sidebar cards are now individual shortcuts: tapping the identity chip opens identity settings, tapping the node card jumps straight to the Node menu (was: both opened generic settings). - Pay screen gains a scan-to-pay QR puck top-right that opens the camera and prefills the recipient while keeping the typed amount (reuses the Home scan + SendFlow::request_scan/prefill_amount path). - Replace the USD-only "fiat preview" with a configurable "Pairing": Off / USD / EUR / GBP / JPY / CNY / Bitcoin / Sats (default USD). price.rs now fetches GRIN against any vs_currency (sats price vs btc, ×1e8) and caches per code; a Settings → Pairing sub-page picks it; the Pay, send, and balance previews all route through one pairing_preview() helper. - Hide the Yellow theme from the picker (cycle is Dark ↔ Light now); the ThemeKind::Yellow tokens stay defined — it's beta, not removed. Verified live on :2 (Default wallet): node card → Node menu; identity chip → identity settings; QR puck renders top-right of Pay; Pairing → Sats shows "≈ 1,912 sats" for 50ツ over a live BTC rate, resets to USD; theme cycles Dark↔Light, never Yellow. 34 lib tests green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -19,4 +19,4 @@ mod release;
|
||||
pub use release::*;
|
||||
|
||||
mod price;
|
||||
pub use price::grin_usd_rate;
|
||||
pub use price::grin_rate;
|
||||
|
||||
+44
-38
@@ -12,30 +12,31 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! GRIN/USD price preview, fetched over Tor and cached.
|
||||
//! GRIN price preview, fetched over Tor and cached per currency.
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::RwLock;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use crate::AppConfig;
|
||||
use crate::tor::Tor;
|
||||
|
||||
/// Cache refresh interval (seconds).
|
||||
const REFRESH_SECS: i64 = 300;
|
||||
|
||||
lazy_static! {
|
||||
/// Cached (rate, fetched_at) and an in-flight flag.
|
||||
static ref RATE: RwLock<Option<(f64, i64)>> = RwLock::new(None);
|
||||
static ref FETCHING: std::sync::atomic::AtomicBool =
|
||||
std::sync::atomic::AtomicBool::new(false);
|
||||
static ref LAST_TRY: std::sync::atomic::AtomicI64 = std::sync::atomic::AtomicI64::new(0);
|
||||
}
|
||||
|
||||
/// Minimum delay between fetch attempts, so a failing fetch (e.g. Tor still
|
||||
/// bootstrapping) does not respawn a thread every frame.
|
||||
/// Minimum delay between fetch attempts for a currency, so a failing fetch
|
||||
/// (e.g. Tor still bootstrapping) does not respawn a thread every frame.
|
||||
const RETRY_SECS: i64 = 30;
|
||||
|
||||
lazy_static! {
|
||||
/// Cached GRIN rates per `vs_currency`: code -> (rate, fetched_at).
|
||||
static ref RATES: RwLock<HashMap<String, (f64, i64)>> = RwLock::new(HashMap::new());
|
||||
/// Currencies with a fetch currently in flight.
|
||||
static ref FETCHING: RwLock<HashSet<String>> = RwLock::new(HashSet::new());
|
||||
/// Last fetch attempt per currency (unix secs).
|
||||
static ref LAST_TRY: RwLock<HashMap<String, i64>> = RwLock::new(HashMap::new());
|
||||
}
|
||||
|
||||
fn now() -> i64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
@@ -43,60 +44,65 @@ fn now() -> i64 {
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Get the cached GRIN/USD rate if fresh, triggering a refresh otherwise.
|
||||
/// Returns `None` until the first successful fetch or when fiat is disabled.
|
||||
pub fn grin_usd_rate() -> Option<f64> {
|
||||
if !AppConfig::fiat_preview() {
|
||||
return None;
|
||||
}
|
||||
let cached = { RATE.read().clone() };
|
||||
/// Get the cached GRIN rate against `vs` (e.g. "usd", "eur", "btc") if fresh,
|
||||
/// triggering a background refresh otherwise. Returns `None` until the first
|
||||
/// successful fetch for that currency.
|
||||
pub fn grin_rate(vs: &str) -> Option<f64> {
|
||||
let cached = { RATES.read().get(vs).cloned() };
|
||||
let needs_refresh = match cached {
|
||||
Some((_, ts)) => now() - ts > REFRESH_SECS,
|
||||
None => true,
|
||||
};
|
||||
if needs_refresh {
|
||||
trigger_refresh();
|
||||
trigger_refresh(vs.to_string());
|
||||
}
|
||||
cached.map(|(rate, _)| rate)
|
||||
}
|
||||
|
||||
/// Spawn a background refresh over Tor (deduplicated).
|
||||
fn trigger_refresh() {
|
||||
use std::sync::atomic::Ordering;
|
||||
/// Spawn a background refresh over Tor for one currency (deduplicated per code).
|
||||
fn trigger_refresh(vs: String) {
|
||||
let t = now();
|
||||
if t - LAST_TRY.load(Ordering::SeqCst) < RETRY_SECS {
|
||||
return;
|
||||
{
|
||||
let last = LAST_TRY.read().get(&vs).copied().unwrap_or(0);
|
||||
if t - last < RETRY_SECS {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if FETCHING.swap(true, Ordering::SeqCst) {
|
||||
return;
|
||||
{
|
||||
let mut fetching = FETCHING.write();
|
||||
if fetching.contains(&vs) {
|
||||
return;
|
||||
}
|
||||
fetching.insert(vs.clone());
|
||||
}
|
||||
LAST_TRY.store(t, Ordering::SeqCst);
|
||||
std::thread::spawn(|| {
|
||||
LAST_TRY.write().insert(vs.clone(), t);
|
||||
std::thread::spawn(move || {
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
rt.block_on(async {
|
||||
if let Some(rate) = fetch_rate().await {
|
||||
let mut w = RATE.write();
|
||||
*w = Some((rate, now()));
|
||||
if let Some(rate) = fetch_rate(&vs).await {
|
||||
RATES.write().insert(vs.clone(), (rate, now()));
|
||||
}
|
||||
});
|
||||
FETCHING.store(false, Ordering::SeqCst);
|
||||
FETCHING.write().remove(&vs);
|
||||
});
|
||||
}
|
||||
|
||||
/// Fetch the GRIN/USD rate from CoinGecko over Tor.
|
||||
async fn fetch_rate() -> Option<f64> {
|
||||
let url =
|
||||
"https://api.coingecko.com/api/v3/simple/price?ids=grin&vs_currencies=usd".to_string();
|
||||
/// Fetch the GRIN/`vs` rate from CoinGecko over Tor.
|
||||
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 Tor.
|
||||
let headers = vec![("User-Agent".to_string(), "goblin-wallet".to_string())];
|
||||
let body = Tor::http_request("GET", url, None, headers).await?;
|
||||
let parsed: Option<f64> = serde_json::from_str::<serde_json::Value>(&body)
|
||||
.ok()
|
||||
.and_then(|doc| doc.get("grin")?.get("usd")?.as_f64());
|
||||
.and_then(|doc| doc.get("grin")?.get(vs)?.as_f64());
|
||||
if parsed.is_none() {
|
||||
log::warn!(
|
||||
"price: unexpected response from rate API (Tor exit blocked?): {}",
|
||||
|
||||
Reference in New Issue
Block a user