Skip to content

fix(nip46): connect relays before showing QR, retry polling for signer - #70

Merged
oth-body merged 1 commit into
masterfrom
fix/nip46-connect-and-retry
Sep 14, 2026
Merged

oth-body merged 1 commit into
masterfrom
fix/nip46-connect-and-retry

Conversation

@oth-body

Copy link
Copy Markdown
Owner

Fixes the 'still refused' NIP-46 login bug. See commit message for full details.

Root cause: relay dial+subscribe happened AFTER the QR was shown (inside OnCheckQR), so the signer's connect event could arrive before subscriptions were active. Also, the TUI only checked once — if it failed, the screen was stuck forever.

Fix: ConnectRelays() now runs inside OnInitQR (before the QR is displayed), and CheckConnection() is a short 3s poll that the TUI retries every second via tea.Tick until the signer connects or the user presses Esc.

All tests pass locally; CI will verify.

The NIP-46 QR login was broken in two ways:

1. Relay dial + subscribe happened inside OnCheckQR (after the QR
   was already displayed). That meant a 10-15 second dial delay
   after the user scanned, during which the signer's connect event
   could arrive before the subscription was active — and be missed.
   The screen would stay on 'Waiting for connection...' forever.

2. checkQRConnection in the TUI ran exactly once. If OnCheckQR
   returned an error (timeout, refused, etc.), the function returned
   nil and nothing ever re-fired the check. The user was stuck.

Fix:

nip46/nip46.go:
- New ConnectRelays(ctx): dials every configured relay in parallel
  and subscribes for kind 24133 events immediately. Called from
  OnInitQR so subscriptions are active before the QR is shown.
- New CheckConnection(timeout): short-polls the merged event
  channel. Returns (pubkey, nil) on success or ('', error) if
  nothing arrived yet. Designed for repeated calling from the TUI.
- Removed WaitForConnection (replaced by the above split).

hoot.go:
- OnInitQR now calls session.ConnectRelays(ctx) before returning
  the URI. By the time the user sees the QR, subscriptions are
  already listening on every healthy relay.
- OnCheckQR calls session.CheckConnection(3s) — a short poll.
  On error it returns the error; the TUI retries.

tui/tui.go:
- checkQRConnection now returns qrRetryMsg on error instead of nil.
- New qrRetryMsg / checkQRTickMsg types. Update() handles
  qrRetryMsg by scheduling a tea.Tick(1s) that fires
  checkQRTickMsg, which re-runs checkQRConnection. This creates
  a polling loop that keeps trying until the signer connects or
  the user presses Esc.
@oth-body
oth-body merged commit e52abfb into master Sep 14, 2026
7 checks passed
@oth-body
oth-body deleted the fix/nip46-connect-and-retry branch September 14, 2026 23:00
oth-body pushed a commit that referenced this pull request Sep 15, 2026
Hoot's NIP-46 sign-in flow uses github.com/nbd-wtf/go-nostr's
nostr.RelayConnect, which delegates to net/http and therefore
honors HTTPS_PROXY / HTTP_PROXY / ALL_PROXY / SOCKS_PROXY env
vars inherited from the parent shell. If the user runs hoot
inside torsocks, a launcher that exports HTTPS_PROXY, or a shell
that already has Tor configured for monerod / curl, the wss://
dials to relay.damus.io / relay.nostr.band / nostr.wine (or
whatever the user picked) get routed through 127.0.0.1:9050.
When nothing is listening there — which is the typical case on
a workstation without an active Tor — every dial fails with
'dial tcp 127.0.0.1:9050: connect: connection refused', which
bubbles up as 'failed to connect to port 9050' in the TUI and
gives the user no actionable signal.

Fix it two ways:

  - nip46.ConnectRelays now refuses to dial at all when any
    *_PROXY env var is set, returning a clear error that names
    the offending env var and the proxy host:port. The user can
    either unset it (or run hoot outside torsocks). This matches
    what every other Nostr TUI does — nak, lume, etc. — none
    of them proxy wss:// connections by default.

  - Up-front validation of every configured relay URL (rejecting
    blanks and non-ws:// / non-wss:// schemes) so a misconfigured
    relays.txt surfaces a clear error instead of a confusing
    websocket one.

Anchored against master after PR #69 / #70 / #72 restructured
the NIP-46 dial path (RelayURL string -> RelayURLs []string,
WaitForConnection -> ConnectRelays + CheckConnection). Tests
updated accordingly: ConnectRelays is the new dial entry point,
and validateRelayURLs is exercised with single-bad-URL and
mixed-good-and-bad inputs.

Tests in nip46/nip46_test.go pin:
  - the proxy env var detection order (HTTPS_PROXY beats
    HTTP_PROXY beats ALL_PROXY; socks fallbacks come last)
  - that malformed or empty proxy values fall through to the
    next valid var (matching Go's stdlib httpproxy behaviour)
  - that ConnectRelays surfaces the env var name and port on
    the user-visible error path (this is the bug's regression
    guard)
  - that blank and non-wss relay URLs are rejected up front
    instead of producing a confusing websocket error later
  - that Session.Close is idempotent when no dial was attempted
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant