1 package protocol
2 3 import (
4 "git.smesh.lol/nostr/pkg/lol/errorf"
5 )
6 7 // Address is an addressable event reference: `<kind>:<pubkey>:<d>`, the value
8 // of an `a` tag. The d part is opaque (NIP-01) and may itself contain colons,
9 // which is how the access-filter address carries a tier and a full asset
10 // address inside its own d value.
11 type Address struct {
12 Kind uint16
13 Pubkey string
14 D string
15 Relay string // optional relay hint, the third element of the tag
16 }
17 18 // String renders the address without the relay hint.
19 func (a *Address) String() (s string) {
20 if a == nil {
21 return ""
22 }
23 return uint16Str(a.Kind) | ":" | a.Pubkey | ":" | a.D
24 }
25 26 // Tag renders the address as an `a` tag, with the relay hint when there is one.
27 func (a *Address) Tag() (t []string) {
28 if a == nil {
29 return nil
30 }
31 if a.Relay == "" {
32 t = []string{:2}
33 t[0] = TagA
34 t[1] = a.String()
35 return
36 }
37 t = []string{:3}
38 t[0] = TagA
39 t[1] = a.String()
40 t[2] = a.Relay
41 return
42 }
43 44 // AssetAddress is the track address an asset reference names.
45 func TrackAddress(pubkey, d string) (a *Address) {
46 return &Address{Kind: KindTrack, Pubkey: pubkey, D: d}
47 }
48 49 func AlbumAddress(pubkey, d string) (a *Address) {
50 return &Address{Kind: KindAlbum, Pubkey: pubkey, D: d}
51 }
52 53 // ParseAddress parses `kind:pubkey:d`. The d part is everything after the second
54 // colon and may contain colons.
55 func ParseAddress(s string) (a *Address, err error) {
56 first := -1
57 second := -1
58 for i := 0; i < len(s); i++ {
59 if s[i] != ':' {
60 continue
61 }
62 if first < 0 {
63 first = i
64 continue
65 }
66 second = i
67 break
68 }
69 if first < 0 || second < 0 {
70 err = errorf.E("address %q is not kind:pubkey:d", s)
71 return
72 }
73 k, kerr := parseUint16(s[:first])
74 if kerr != nil {
75 err = errorf.E("address %q: %s", s, kerr.Error())
76 return
77 }
78 pk := s[first+1 : second]
79 if !IsHex64(pk) {
80 err = errorf.E("address %q: pubkey is not 64 hex characters", s)
81 return
82 }
83 d := s[second+1:]
84 if d == "" {
85 err = errorf.E("address %q: empty d tag", s)
86 return
87 }
88 a = &Address{Kind: k, Pubkey: pk, D: d}
89 return
90 }
91 92 // AddressFromTag parses an `a` tag, taking the relay hint when present.
93 func AddressFromTag(tag []string) (a *Address, err error) {
94 if len(tag) < 2 {
95 err = errorf.E("a tag needs a value")
96 return
97 }
98 // NIP-22 uses an uppercase A for the root scope of a comment and a
99 // lowercase a for the parent; the value syntax is identical.
100 if tag[0] != TagA && tag[0] != TagARoot {
101 err = errorf.E("tag %q is not an a tag", tag[0])
102 return
103 }
104 a, err = ParseAddress(tag[1])
105 if err != nil {
106 return
107 }
108 if len(tag) > 2 {
109 a.Relay = tag[2]
110 }
111 return
112 }
113 114 // IsHex64 reports whether s is exactly 64 lowercase hex characters. Filter
115 // lists and address pubkeys require exactly that: uppercase hex or a wrong
116 // length is a different string to a relay index.
117 func IsHex64(s string) (ok bool) {
118 if len(s) != 64 {
119 return false
120 }
121 for i := 0; i < len(s); i++ {
122 c := s[i]
123 if (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') {
124 continue
125 }
126 return false
127 }
128 return true
129 }
130 131 // IsHex reports whether s is lowercase hex of any even length.
132 func IsHex(s string) (ok bool) {
133 if len(s) == 0 || len(s)%2 != 0 {
134 return false
135 }
136 for i := 0; i < len(s); i++ {
137 c := s[i]
138 if (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') {
139 continue
140 }
141 return false
142 }
143 return true
144 }
145 146 // parseUint16 parses a decimal unsigned 16-bit integer. strconv is available
147 // in the stdlib but its error text is not what the relay logs want, and this
148 // also rejects a leading '+' and whitespace, which ParseUint accepts.
149 func parseUint16(s string) (n uint16, err error) {
150 if len(s) == 0 || len(s) > 5 {
151 err = errorf.E("%q is not a kind", s)
152 return
153 }
154 v := int32(0)
155 for i := 0; i < len(s); i++ {
156 c := s[i]
157 if c < '0' || c > '9' {
158 err = errorf.E("%q is not a kind", s)
159 return
160 }
161 v = v*10 + int32(c-'0')
162 }
163 if v > 65535 {
164 err = errorf.E("kind %q is out of range", s)
165 return
166 }
167 n = uint16(v)
168 return
169 }
170 171 // uint16Str renders a decimal unsigned 16-bit integer.
172 func uint16Str(n uint16) (s string) {
173 if n == 0 {
174 return "0"
175 }
176 buf := []byte{:0:5}
177 for n > 0 {
178 buf = push(buf, byte('0'+n%10))
179 n = n / 10
180 }
181 out := []byte{:len(buf)}
182 j := int32(0)
183 for i := int32(len(buf)) - 1; i >= 0; i-- {
184 out[j] = buf[i]
185 j++
186 }
187 return string(out)
188 }
189 190 // intStr renders a decimal int64. The spec writes every count and timestamp as
191 // a string tag value.
192 func intStr(n int64) (s string) {
193 if n == 0 {
194 return "0"
195 }
196 neg := n < 0
197 if neg {
198 n = -n
199 }
200 buf := []byte{:0:20}
201 for n > 0 {
202 buf = push(buf, byte('0'+n%10))
203 n = n / 10
204 }
205 size := len(buf)
206 if neg {
207 size++
208 }
209 out := []byte{:size}
210 j := int32(0)
211 if neg {
212 out[j] = '-'
213 j++
214 }
215 for i := int32(len(buf)) - 1; i >= 0; i-- {
216 out[j] = buf[i]
217 j++
218 }
219 return string(out)
220 }
221 222 // ParseInt64 parses a decimal integer, accepting a leading '-'. Tag values are
223 // strings on the wire; an empty value is an error, not a zero.
224 func ParseInt64(s string) (n int64, err error) {
225 if len(s) == 0 {
226 err = errorf.E("empty integer")
227 return
228 }
229 i := int32(0)
230 neg := false
231 if s[0] == '-' {
232 neg = true
233 i++
234 if len(s) == 1 {
235 err = errorf.E("%q is not an integer", s)
236 return
237 }
238 }
239 for ; i < int32(len(s)); i++ {
240 c := s[i]
241 if c < '0' || c > '9' {
242 err = errorf.E("%q is not an integer", s)
243 return
244 }
245 n = n*10 + int64(c-'0')
246 }
247 if neg {
248 n = -n
249 }
250 return
251 }
252