1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
|
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] + "..."
}
|