diff --git a/README.md b/README.md index 9dfa2df..fce71a6 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,9 @@ Goblin is a fork of the **Grim** egui GRIN wallet: it keeps Grim's full GRIN nod ## What it does - **Send to people** — pay a `@username` or `npub`; the GRIN slatepack travels as a [NIP-17](https://nips.nostr.com/17) gift-wrapped DM ([kind 1059](https://nostrbook.dev/kinds/1059)) over the Nym mixnet and is applied automatically by the recipient's wallet. No files to swap, no need to both be online at once. +- **Manual slatepacks too** — when you need to pay or get paid without a handle, **Settings → Wallet → Slatepacks** exposes the classic by-hand flow: create a slatepack to send, or paste one to receive, finalize, or pay. - **In-app identity** — a nostr payment key that is deliberately *not* part of your seed, so you can rotate it any time to stay unlinkable without touching your funds. An optional human-readable `@name` (and hosted avatar) comes from the goblin.st identity service. -- **Private by construction** — GRIN's address-less, confidential chain; every relay and HTTP request (relays, NIP-05 lookups, price, avatars) routed through the [Nym mixnet](https://nym.com) via a bundled `nym-socks5-client` sidecar, so nothing touches the clear net; keys, names and history stay on your device. +- **Private by construction** — GRIN's address-less, confidential chain; every relay and HTTP request (relays, NIP-05 lookups, price, avatars) is routed through the [Nym mixnet](https://nym.com), so nothing touches the clear net; keys, names and history stay on your device. - **Configurable amount pairing** — show balances against a world currency, Bitcoin, or sats (rates fetched over the mixnet), or turn the preview off. - **Cross-platform** — Linux, macOS, Windows, Android, built in pure Rust on [egui](https://github.com/emilk/egui). @@ -40,13 +41,16 @@ Both parties only need one relay in common. The default set is the Goblin relay ### Desktop (Linux / macOS / Windows) +Goblin links the [Nym mixnet](https://nym.com) SDK **in-process** — the wallet is a single self-contained binary, no sidecar. The SDK builds from a sibling `../nym` checkout (a pinned nym tree with a small Android TLS patch): + ``` +git clone --branch goblin https://git.us-ea.st/GRIN/nym ../nym git submodule update --init --recursive cargo build --release ./target/release/goblin ``` -Goblin routes all of its traffic over the [Nym mixnet](https://nym.com) using a `nym-socks5-client` sidecar that runs alongside the wallet and exposes a local SOCKS5 proxy on `127.0.0.1:1080`. Ship the `nym-socks5-client` binary next to the `goblin` executable (or point `GOBLIN_NYM_BIN` at it), and set the network requester it routes through via `GOBLIN_NYM_PROVIDER` (or bake it into `NETWORK_REQUESTER` in `src/nym/sidecar.rs`). If a SOCKS5 endpoint is already listening on `127.0.0.1:1080`, Goblin reuses it. +All wallet traffic — nostr relays, NIP-05 lookups, price and avatar fetches — is routed over the mixnet through a network requester (the default is baked into `NETWORK_REQUESTER` in `src/nym/sidecar.rs`); the SDK's SOCKS5 listener is run in-process on `127.0.0.1:1080`. If something is already listening there, Goblin reuses it. ### Android diff --git a/src/gui/views/goblin/avatars.rs b/src/gui/views/goblin/avatars.rs index f8604c0..cefdd86 100644 --- a/src/gui/views/goblin/avatars.rs +++ b/src/gui/views/goblin/avatars.rs @@ -30,7 +30,7 @@ enum Fetched { Found(String, Vec), /// The server confirmed the name has no avatar. Absent, - /// The probe failed (network/Tor) — do NOT cache; retry later. + /// The probe failed (network) — do NOT cache; retry later. Failed, } type FetchResult = (String, Fetched); @@ -146,9 +146,8 @@ impl AvatarTextures { self.cache.mark_absent(&name); self.textures.insert(name, None); } - // Network/Tor failure: leave the entry stale so the next - // frame retries once a circuit is healthy. Never cache it as - // a confirmed "no avatar". + // Network failure: leave the entry stale so the next frame + // retries. Never cache it as a confirmed "no avatar". Fetched::Failed => {} } ctx.request_repaint(); diff --git a/src/gui/views/goblin/mod.rs b/src/gui/views/goblin/mod.rs index 4ba5119..95aa3ac 100644 --- a/src/gui/views/goblin/mod.rs +++ b/src/gui/views/goblin/mod.rs @@ -1701,7 +1701,7 @@ impl GoblinWalletView { Vec2::new(half, 56.0), )), |ui| { - // Copy the grin1 slatepack address for manual/Tor exchange. + // Copy the grin1 slatepack address for manual exchange. let label = if copied1 { format!("{} Copied", CHECK) } else { @@ -3069,7 +3069,7 @@ impl GoblinWalletView { } } - /// Inline username-claim widget (availability check + register over Tor). + /// Inline username-claim widget (availability check + registration). fn claim_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) { let t = theme::tokens(); // Poll the worker result; avatar invalidation happens after the diff --git a/src/gui/views/goblin/widgets.rs b/src/gui/views/goblin/widgets.rs index de698ff..f560382 100644 --- a/src/gui/views/goblin/widgets.rs +++ b/src/gui/views/goblin/widgets.rs @@ -433,11 +433,9 @@ pub fn chip_outline(ui: &mut Ui, label: &str) -> Response { resp } -/// Paint a QR code for `text` with the goblin mark centered, per the -/// design's receive card. Always dark modules on a white plate, whatever the -/// theme: inverted (light-on-dark) codes fail to decode in a number of -/// scanner apps. Encoding a short URI is microseconds, so this is done -/// synchronously each frame; modules are plain painter rects. +/// Paint a QR code for `text` with the goblin mark centered. Always dark modules +/// on a white plate, whatever the theme — inverted codes fail to decode in many +/// scanners. Encoded synchronously each frame; modules are plain painter rects. pub fn qr_code(ui: &mut Ui, text: &str, size: f32) { let plate = Color32::WHITE; let ink = Color32::from_rgb(0x0E, 0x0E, 0x0C); @@ -452,10 +450,9 @@ pub fn qr_code(ui: &mut Ui, text: &str, size: f32) { let rect = outer.shrink(pad); let n = qr.size(); let cell = size / n as f32; - // Full cells with no inter-module gap: at receive-card density (~4.5px - // cells) even a 0.5px gap fragments the finder patterns and scanners - // fail to detect the code at all (probed with rqrr). Corner rounding - // only when cells are big enough that the notching can't matter. + // Full cells, no inter-module gap: at receive-card density (~4.5px cells) even + // a 0.5px gap fragments the finder patterns and scanners fail. Round corners + // only when cells are large enough that the notching can't matter. let radius = if cell >= 6.0 { (cell * 0.3) as u8 } else { 0 }; for y in 0..n { for x in 0..n { @@ -469,11 +466,9 @@ pub fn qr_code(ui: &mut Ui, text: &str, size: f32) { } } } - // Goblin mark on a yellow backing square in the center, same 19% footprint - // the white version was tuned to (at 26%, zbar-class scanners fail on the - // glyph; 19% passes everything probed). Yellow's luminance reads as "light" - // to a scanner just like white, so the obscured center is recovered by the - // High ECC exactly as before — only the colour changes. + // Goblin mark on a yellow backing square in the center, 19% footprint (larger + // obscures too many modules for a reliable decode). Yellow reads as "light" to + // a scanner like white, so the covered center is recovered by the High ECC. let t = theme::tokens(); let backing = size * 0.19; let b_rect = egui::Rect::from_center_size(rect.center(), Vec2::splat(backing)); diff --git a/src/nostr/client.rs b/src/nostr/client.rs index 1b7dde8..a275afb 100644 --- a/src/nostr/client.rs +++ b/src/nostr/client.rs @@ -517,17 +517,16 @@ async fn run_service(svc: Arc, wallet: Wallet) { warn!("nostr: add relay {relay} failed: {e}"); } } - // Wait for the bundled Nym sidecar to be listening before dialing relays. + // Wait for the in-process Nym SOCKS5 proxy (:1080) before dialing relays. // `warm_up()` starts it at launch, but a fast wallet-open can beat the cold - // mixnet bootstrap — and dialing relays before :1080 is up makes every relay - // fail and drop into nostr-sdk's (backing-off) reconnect, so the wallet sits - // on "Connecting…" long after the mixnet is actually ready. Once the sidecar - // is warm this returns immediately. + // mixnet bootstrap — and dialing before it's up drops every relay into + // nostr-sdk's backing-off reconnect, leaving the wallet on "Connecting…" long + // after the mixnet is actually ready. Once it's warm this returns immediately. for i in 0..60u32 { if nym_socks_ready().await { if i > 0 { info!( - "nostr: Nym sidecar ready after ~{}ms, dialing relays", + "nostr: Nym proxy ready after ~{}ms, dialing relays", i * 500 ); } @@ -542,10 +541,8 @@ async fn run_service(svc: Arc, wallet: Wallet) { *w_client = Some(client.clone()); } - // Instrumentation: log the moment the first relay actually reaches Connected - // over the mixnet, measured from the connect() call. Cold-start latency is - // then read off the wall clock (paired with the "Nym sidecar ready after - // ~Nms" line above) instead of guessed. Non-blocking; exits on first success. + // Log when the first relay reaches Connected over the mixnet, measured from + // the connect() call. Non-blocking; exits on first success. { let client_probe = client.clone(); let svc_probe = svc.clone(); @@ -661,7 +658,7 @@ async fn run_service(svc: Arc, wallet: Wallet) { client.disconnect().await; } -/// Quick, non-blocking check that the Nym SOCKS5 sidecar is accepting +/// Quick, non-blocking check that the Nym SOCKS5 proxy is accepting /// connections on its loopback port (i.e. the mixnet is ready to carry traffic). async fn nym_socks_ready() -> bool { matches!( diff --git a/src/nostr/identity.rs b/src/nostr/identity.rs index bed17ce..7fa3d3d 100644 --- a/src/nostr/identity.rs +++ b/src/nostr/identity.rs @@ -27,7 +27,7 @@ use std::path::PathBuf; #[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Debug)] pub enum IdentitySource { /// NIP-06 derivation from the wallet BIP-39 mnemonic (legacy: binds the - /// identity to the seed forever; superseded by `Random`). + /// identity to the seed forever). Derived, /// Imported nsec. Imported, diff --git a/src/nostr/nip05.rs b/src/nostr/nip05.rs index 3c3dee5..dc6c950 100644 --- a/src/nostr/nip05.rs +++ b/src/nostr/nip05.rs @@ -13,7 +13,7 @@ // limitations under the License. //! NIP-05 username resolution/verification and goblin.st registration, -//! all HTTP routed through the Nym mixnet (the local SOCKS5 sidecar). Nothing +//! all HTTP routed through the Nym mixnet (the local SOCKS5 proxy). Nothing //! here touches clearnet. use base64::Engine; diff --git a/src/nym/mod.rs b/src/nym/mod.rs index 3b6e61a..9977f0a 100644 --- a/src/nym/mod.rs +++ b/src/nym/mod.rs @@ -28,7 +28,7 @@ use std::time::Duration; pub use sidecar::warm_up; pub use transport::NymWebSocketTransport; -/// Local SOCKS5 endpoint exposed by the bundled `nym-socks5-client` sidecar. +/// Local SOCKS5 endpoint exposed by the in-process Nym SOCKS5 client. /// `socks5h` keeps DNS resolution inside the proxy so the destination host is /// never resolved on the clear. pub const SOCKS5_HOST: &str = "127.0.0.1"; diff --git a/src/nym/sidecar.rs b/src/nym/sidecar.rs index 7a8d25a..56d77f0 100644 --- a/src/nym/sidecar.rs +++ b/src/nym/sidecar.rs @@ -17,9 +17,7 @@ //! client on a private internal tokio runtime, exposing the mixnet at //! `127.0.0.1:1080`; every relay + HTTP request in the app is pointed at that //! loopback port, so all traffic egresses through the 5-hop mixnet to our network -//! requester. Nothing goes clearnet. This mirrors how GRIM links arti/Tor -//! in-process — the loopback is a private interface inside one process, not a -//! separate program. +//! requester. Nothing goes clearnet. use std::net::{SocketAddr, TcpStream}; use std::path::PathBuf; @@ -33,17 +31,15 @@ use nym_sdk::mixnet::{MixnetClientBuilder, Socks5, Socks5MixnetClient, StoragePa use super::{SOCKS5_HOST, SOCKS5_PORT}; /// Network requester (the mixnet exit) Goblin routes through — the SOCKS5 -/// client's `--provider`. This is the always-on requester we run on us-ea.st -/// (standard Nym exit policy, which permits the wss/443 + HTTPS hosts Goblin -/// needs). Overridable at runtime with `GOBLIN_NYM_PROVIDER`. If left empty, the -/// in-process client isn't started, but an already-running SOCKS5 endpoint (a dev -/// sidecar / system service on :1080) is still reused. +/// client's `--provider`. Standard Nym exit policy, which permits the wss/443 + +/// HTTPS hosts Goblin needs. Overridable at runtime with `GOBLIN_NYM_PROVIDER`. If +/// left empty, the in-process client isn't started, but an already-running SOCKS5 +/// endpoint on :1080 is still reused. pub const NETWORK_REQUESTER: &str = "5ibBQ9SS1er3tks5tfmrzCQ29qU1uBSvZN2dUwLKPRwu.HdbktiMVniUyaKBnorFVXLRHdwRb8iG9dV481r5xyopV@2RmEBKhQHsqvw5sjnnt2Bhpy96MPDUkbfWkT6r2RWNCR"; /// Pre-warm the mixnet transport in the background so relays / NIP-05 / price are -/// ready by first use. If a SOCKS5 endpoint -/// is already listening (a dev sidecar, or a system-managed service), it is reused -/// as-is; otherwise the in-process client is started. +/// ready by first use. If a SOCKS5 endpoint is already listening on :1080 it is +/// reused as-is; otherwise the in-process client is started. pub fn warm_up() { thread::spawn(|| { if port_open(Duration::from_millis(300)) { diff --git a/src/nym/transport.rs b/src/nym/transport.rs index d1a34ff..30558d9 100644 --- a/src/nym/transport.rs +++ b/src/nym/transport.rs @@ -49,7 +49,7 @@ fn terr(msg: impl Into) -> TransportError { TransportError::backend(NymTransportError(msg.into())) } -/// Nostr websocket transport over the local Nym SOCKS5 sidecar. +/// Nostr websocket transport over the local Nym SOCKS5 proxy. #[derive(Debug, Clone, Copy, Default)] pub struct NymWebSocketTransport;