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:
@@ -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
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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!(
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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)) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user