nip19.go raw
1 package nostr
2
3 import (
4 "encoding/binary"
5 "encoding/hex"
6 "fmt"
7 "strings"
8 )
9
10 // Nevent returns the NIP-19 bech32-encoded nevent string for an event,
11 // including relay hints. This is the shareable identifier that Nostr
12 // clients use to locate and display an event.
13 func Nevent(eventIDHex string, relays []string, authorHex string, kind int) (string, error) {
14 id, err := hex.DecodeString(eventIDHex)
15 if err != nil || len(id) != 32 {
16 return "", fmt.Errorf("invalid event id hex")
17 }
18
19 var tlv []byte
20
21 // TLV type 0: event id (32 bytes).
22 tlv = append(tlv, 0, 32)
23 tlv = append(tlv, id...)
24
25 // TLV type 1: relay URL(s).
26 for _, r := range relays {
27 b := []byte(r)
28 tlv = append(tlv, 1, byte(len(b)))
29 tlv = append(tlv, b...)
30 }
31
32 // TLV type 2: author pubkey (32 bytes).
33 if authorHex != "" {
34 pub, err := hex.DecodeString(authorHex)
35 if err == nil && len(pub) == 32 {
36 tlv = append(tlv, 2, 32)
37 tlv = append(tlv, pub...)
38 }
39 }
40
41 // TLV type 3: kind (4 bytes big-endian).
42 var kb [4]byte
43 binary.BigEndian.PutUint32(kb[:], uint32(kind))
44 tlv = append(tlv, 3, 4)
45 tlv = append(tlv, kb[:]...)
46
47 return bech32Encode("nevent", tlv)
48 }
49
50 // NpubToHex decodes an npub1... bech32 string to a 32-byte hex pubkey.
51 func NpubToHex(npub string) (string, error) {
52 hrp, data, err := bech32Decode(npub)
53 if err != nil {
54 return "", fmt.Errorf("bech32 decode: %w", err)
55 }
56 if hrp != "npub" {
57 return "", fmt.Errorf("expected hrp 'npub', got '%s'", hrp)
58 }
59 bytes, err := convertBits(data, 5, 8, false)
60 if err != nil {
61 return "", fmt.Errorf("convert bits: %w", err)
62 }
63 if len(bytes) != 32 {
64 return "", fmt.Errorf("expected 32 bytes, got %d", len(bytes))
65 }
66 return hex.EncodeToString(bytes), nil
67 }
68
69 // PubkeyToNpub encodes a 32-byte hex pubkey to npub1... bech32 format.
70 func PubkeyToNpub(hexPub string) (string, error) {
71 b, err := hex.DecodeString(hexPub)
72 if err != nil {
73 return "", err
74 }
75 if len(b) != 32 {
76 return "", fmt.Errorf("expected 32 bytes, got %d", len(b))
77 }
78 return bech32Encode("npub", b)
79 }
80
81 // --- bech32 encoding/decoding (BIP-173) ---
82
83 const bech32Charset = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
84
85 func bech32Encode(hrp string, data []byte) (string, error) {
86 // Convert 8-bit data to 5-bit groups.
87 conv, err := convertBits(data, 8, 5, true)
88 if err != nil {
89 return "", err
90 }
91
92 // Compute checksum.
93 chk := bech32Checksum(hrp, conv)
94
95 var b strings.Builder
96 b.WriteString(hrp)
97 b.WriteByte('1')
98 for _, d := range conv {
99 b.WriteByte(bech32Charset[d])
100 }
101 for _, d := range chk {
102 b.WriteByte(bech32Charset[d])
103 }
104 return b.String(), nil
105 }
106
107 func bech32Decode(s string) (string, []byte, error) {
108 s = strings.ToLower(s)
109 pos := strings.LastIndex(s, "1")
110 if pos < 1 || pos+7 > len(s) {
111 return "", nil, fmt.Errorf("invalid bech32 separator position")
112 }
113 hrp := s[:pos]
114 dataStr := s[pos+1:]
115
116 var data []byte
117 for _, c := range dataStr {
118 idx := strings.IndexRune(bech32Charset, c)
119 if idx < 0 {
120 return "", nil, fmt.Errorf("invalid bech32 character: %c", c)
121 }
122 data = append(data, byte(idx))
123 }
124 if !bech32VerifyChecksum(hrp, data) {
125 return "", nil, fmt.Errorf("invalid bech32 checksum")
126 }
127 return hrp, data[:len(data)-6], nil
128 }
129
130 func bech32VerifyChecksum(hrp string, data []byte) bool {
131 values := append(bech32HRPExpand(hrp), data...)
132 return bech32Polymod(values) == 1
133 }
134
135 func bech32Polymod(values []byte) uint32 {
136 gen := [5]uint32{0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3}
137 chk := uint32(1)
138 for _, v := range values {
139 top := chk >> 25
140 chk = (chk&0x1ffffff)<<5 ^ uint32(v)
141 for i := range 5 {
142 if (top>>uint(i))&1 == 1 {
143 chk ^= gen[i]
144 }
145 }
146 }
147 return chk
148 }
149
150 func bech32HRPExpand(hrp string) []byte {
151 ret := make([]byte, 0, len(hrp)*2+1)
152 for _, c := range hrp {
153 ret = append(ret, byte(c>>5))
154 }
155 ret = append(ret, 0)
156 for _, c := range hrp {
157 ret = append(ret, byte(c&31))
158 }
159 return ret
160 }
161
162 func bech32Checksum(hrp string, data []byte) []byte {
163 values := append(bech32HRPExpand(hrp), data...)
164 values = append(values, 0, 0, 0, 0, 0, 0)
165 polymod := bech32Polymod(values) ^ 1
166 chk := make([]byte, 6)
167 for i := range 6 {
168 chk[i] = byte((polymod >> uint(5*(5-i))) & 31)
169 }
170 return chk
171 }
172
173 func convertBits(data []byte, fromBits, toBits uint, pad bool) ([]byte, error) {
174 acc := uint32(0)
175 bits := uint(0)
176 maxv := uint32((1 << toBits) - 1)
177 var ret []byte
178
179 for _, d := range data {
180 acc = (acc << fromBits) | uint32(d)
181 bits += fromBits
182 for bits >= toBits {
183 bits -= toBits
184 ret = append(ret, byte((acc>>bits)&maxv))
185 }
186 }
187
188 if pad {
189 if bits > 0 {
190 ret = append(ret, byte((acc<<(toBits-bits))&maxv))
191 }
192 } else if bits >= fromBits {
193 return nil, fmt.Errorf("excess padding")
194 } else if (acc<<(toBits-bits))&maxv != 0 {
195 return nil, fmt.Errorf("non-zero padding")
196 }
197
198 return ret, nil
199 }
200