package main import ( "context" "encoding/hex" "flag" "fmt" "log" "os" "time" "fiatjaf.com/nostr" "fiatjaf.com/nostr/nip19" ) const keyFile = "testclient.key" func main() { relayURL := flag.String("relay", "ws://localhost:8080", "Relay WebSocket URL") flag.Parse() // Load or generate keypair sk, err := loadOrGenerateKey() if err != nil { log.Fatalf("Failed to load/generate key: %v", err) } pubkey := nostr.GetPublicKey(sk) npub := nip19.EncodeNpub(pubkey) log.Printf("Pubkey (hex): %s", hex.EncodeToString(pubkey[:])) log.Printf("Pubkey (npub): %s", npub) // Connect to relay with NIP-42 auth handler log.Printf("Connecting to %s...", *relayURL) // Channel to signal when AUTH completes authCompleted := make(chan error, 1) opts := nostr.RelayOptions{ // NIP-42 AUTH handler - signs auth challenges automatically AuthHandler: func(ctx context.Context, r *nostr.Relay, authEvent *nostr.Event) error { log.Printf("AUTH challenge received! Signing auth event...") if err := authEvent.Sign(sk); err != nil { authErr := fmt.Errorf("failed to sign auth event: %w", err) authCompleted <- authErr return authErr } log.Printf("AUTH event signed and sent") authCompleted <- nil return nil }, // Notice handler - logs relay notices NoticeHandler: func(r *nostr.Relay, notice string) { log.Printf("NOTICE from relay: %s", notice) }, } relay, err := nostr.RelayConnect(context.Background(), *relayURL, opts) if err != nil { log.Fatalf("Failed to connect: %v", err) } defer relay.Close() log.Printf("Connected! (NIP-42 auth enabled)") // Try to publish an event (will trigger AUTH if required) log.Printf("Publishing test event...") event := nostr.Event{ PubKey: pubkey, CreatedAt: nostr.Now(), Kind: nostr.KindTextNote, Tags: nostr.Tags{}, Content: fmt.Sprintf("NIP-42 test from testclient at %s", time.Now().Format(time.RFC3339)), } event.Sign(sk) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() // Try to publish - this may trigger AUTH if relay requires it err = relay.Publish(ctx, event) if err != nil { // Publish failed - likely due to AUTH requirement log.Printf("First publish attempt failed: %v", err) // Wait for AUTH to complete log.Printf("Waiting for AUTH to complete...") select { case authErr := <-authCompleted: if authErr != nil { log.Fatalf("AUTH failed: %v", authErr) } log.Printf("AUTH completed successfully") case <-ctx.Done(): log.Fatalf("Timeout waiting for AUTH") } // Retry publish now that we're authenticated log.Printf("Retrying publish after successful auth...") err = relay.Publish(ctx, event) if err != nil { log.Printf("Publish error after retry: %v", err) } else { log.Printf("Event published successfully! ID: %s", hex.EncodeToString(event.ID[:])) } } else { // Publish succeeded immediately - no AUTH required log.Printf("Event published successfully! ID: %s", hex.EncodeToString(event.ID[:])) } // Query events log.Printf("Querying recent events (kind 1, limit 10)...") filters := nostr.Filter{ Kinds: []nostr.Kind{nostr.KindTextNote}, Limit: 10, } queryCtx, queryCancel := context.WithTimeout(context.Background(), 5*time.Second) defer queryCancel() sub, err := relay.Subscribe(queryCtx, filters, nostr.SubscriptionOptions{}) if err != nil { log.Printf("Subscribe error: %v", err) return } count := 0 timeout := time.After(3 * time.Second) log.Printf("Listening for events...") for { select { case evt, ok := <-sub.Events: if !ok { goto done } count++ log.Printf(" Event %d: kind=%d author=%s content=%q", count, evt.Kind, hex.EncodeToString(evt.PubKey[:])[:16], truncate(evt.Content, 50)) if count >= 10 { goto done } case <-sub.EndOfStoredEvents: log.Printf("End of stored events (EOSE)") goto done case <-timeout: log.Printf("Query timeout") goto done case <-queryCtx.Done(): goto done } } done: log.Printf("Retrieved %d events total", count) sub.Close() log.Printf("Test complete!") } // loadOrGenerateKey loads an existing key or generates a new one func loadOrGenerateKey() (nostr.SecretKey, error) { // Try to load existing key data, err := os.ReadFile(keyFile) if err == nil { skHex := string(data) skBytes, err := hex.DecodeString(skHex) if err == nil && len(skBytes) == 32 { var sk nostr.SecretKey copy(sk[:], skBytes) log.Printf("Loaded existing key from %s", keyFile) return sk, nil } } // Generate new key sk := nostr.Generate() skHex := hex.EncodeToString(sk[:]) if err := os.WriteFile(keyFile, []byte(skHex), 0o600); err != nil { return sk, fmt.Errorf("failed to save key: %w", err) } log.Printf("Generated new key and saved to %s", keyFile) return sk, nil } // truncate truncates a string to maxLen characters func truncate(s string, maxLen int) string { if len(s) <= maxLen { return s } return s[:maxLen] + "..." }