1
0
forked from GRIN/grim

Build 70: refresh README + clean stale comments for the public release

README: drop the removed nym-socks5-client sidecar story (the SDK is linked
in-process now), add the in-process build steps + the manual-slatepack feature.

Comments: replace stale "sidecar" wording with the in-process SOCKS5 proxy,
drop leftover Tor references (Goblin routes over Nym), and trim the chattiest
working-notes to terse rationale.
This commit is contained in:
2ro
2026-06-14 13:02:03 -04:00
parent 5445b48a69
commit 851ae1c565
10 changed files with 39 additions and 48 deletions
+6 -2
View File
@@ -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
+3 -4
View File
@@ -30,7 +30,7 @@ enum Fetched {
Found(String, Vec<u8>),
/// 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();
+2 -2
View File
@@ -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
+9 -14
View File
@@ -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));
+8 -11
View File
@@ -517,17 +517,16 @@ async fn run_service(svc: Arc<NostrService>, 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<NostrService>, 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<NostrService>, 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!(
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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";
+7 -11
View File
@@ -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)) {
+1 -1
View File
@@ -49,7 +49,7 @@ fn terr(msg: impl Into<String>) -> 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;