access.mx raw
1 package protocol
2
3 import (
4 "git.smesh.lol/nostr/pkg/lol/errorf"
5 )
6
7 // FilterTier is the parsed d value of an access filter. The three forms are
8 // fan:<asset-address> (the owner's direct buyers), retail:<asset-address> (a
9 // retailer's per-asset buyers) and sub:<retailer-pubkey>:<channel-d> (a
10 // retailer's active subscribers). The asset address is itself a full
11 // kind:pubkey:d string, so the d value contains colons by design.
12 type FilterTier struct {
13 Tier string
14 Asset *Address // fan, retail
15 Retailer string // sub
16 Channel string // sub
17 }
18
19 func (t *FilterTier) String() (s string) {
20 if t == nil {
21 return ""
22 }
23 switch t.Tier {
24 case TierSub:
25 return TierSub | ":" | t.Retailer | ":" | t.Channel
26 }
27 if t.Asset == nil {
28 return t.Tier | ":"
29 }
30 return t.Tier | ":" | t.Asset.String()
31 }
32
33 // ParseFilterD parses the d value of an access filter.
34 func ParseFilterD(d string) (t *FilterTier, err error) {
35 i := indexByte(d, ':')
36 if i < 0 {
37 err = errorf.E("filter d %q has no tier", d)
38 return
39 }
40 tier := d[:i]
41 if !AccessTier(tier) {
42 err = errorf.E("filter d %q: %q is not a tier", d, tier)
43 return
44 }
45 rest := d[i+1:]
46 if tier == TierSub {
47 j := indexByte(rest, ':')
48 if j < 0 {
49 err = errorf.E("filter d %q: a subscription filter needs a retailer and a channel", d)
50 return
51 }
52 retailer := rest[:j]
53 if !IsHex64(retailer) {
54 err = errorf.E("filter d %q: retailer pubkey is not 64 hex characters", d)
55 return
56 }
57 if rest[j+1:] == "" {
58 err = errorf.E("filter d %q: empty channel", d)
59 return
60 }
61 t = &FilterTier{Tier: TierSub, Retailer: retailer, Channel: rest[j+1:]}
62 return
63 }
64 asset, aerr := ParseAddress(rest)
65 if aerr != nil {
66 err = errorf.E("filter d %q: %s", d, aerr.Error())
67 return
68 }
69 t = &FilterTier{Tier: tier, Asset: asset}
70 return
71 }
72
73 // FanFilterD is the d value an owner publishes for an asset.
74 func FanFilterD(asset *Address) (s string) {
75 return TierFan | ":" | asset.String()
76 }
77
78 // RetailFilterD is the d value a retailer publishes for an asset it sold.
79 func RetailFilterD(asset *Address) (s string) {
80 return TierRetail | ":" | asset.String()
81 }
82
83 // SubFilterD is the d value a retailer publishes for a subscription channel.
84 func SubFilterD(retailer, channel string) (s string) {
85 return TierSub | ":" | retailer | ":" | channel
86 }
87
88 // AccessFilter is the payload of a kind 32217 event: the signed probabilistic
89 // entitlement set for one asset and tier. The set itself lives in a Blossom
90 // blob named by x; the event carries the parameters that bind the bytes.
91 type AccessFilter struct {
92 D string // <tier>:<asset-address>, or the subscription form
93 Filter string // bloom
94 Fp string // false-positive rate, <= 1e-6, spec minimum 1e-9
95 N string // declared element count
96 K string // hash-function count, round(-log2(fp))
97 X string // sha256 of the filter blob
98 Server string // Blossom hint; falls back to the owner's server list
99 Mints []string // accepted Cashu mints; "lightning" for a direct leg
100 Retailers []string // authorized retailer pubkeys
101 Expiration string // unix seconds; absent means permanent
102 Catalog []string // subscription coverage: covered asset addresses
103 Content string
104 Extra [][]string
105 }
106
107 func (f *AccessFilter) Tags() (tags [][]string) {
108 tags = appendKV(nil, TagD, f.D)
109 tags = appendKV(tags, TagFilter, f.Filter)
110 tags = appendKV(tags, TagFp, f.Fp)
111 tags = appendKV(tags, TagN, f.N)
112 tags = appendKV(tags, TagK, f.K)
113 tags = appendKV(tags, TagX, f.X)
114 tags = appendKV(tags, TagServer, f.Server)
115 for i := range f.Mints {
116 tags = appendKV(tags, TagMint, f.Mints[i])
117 }
118 for i := range f.Retailers {
119 tags = appendKV(tags, TagRetailer, f.Retailers[i])
120 }
121 tags = appendKV(tags, TagExpiration, f.Expiration)
122 for i := range f.Catalog {
123 tags = appendKV(tags, TagCatalog, f.Catalog[i])
124 }
125 tags = appendTags(tags, f.Extra)
126 return
127 }
128
129 func ParseAccessFilter(tags [][]string, content string) (f *AccessFilter, err error) {
130 f = &AccessFilter{Content: content}
131 for i := range tags {
132 tg := tags[i]
133 if len(tg) < 1 {
134 continue
135 }
136 switch tg[0] {
137 case TagD:
138 f.D = elem(tg, 1)
139 case TagFilter:
140 f.Filter = elem(tg, 1)
141 case TagFp:
142 f.Fp = elem(tg, 1)
143 case TagN:
144 f.N = elem(tg, 1)
145 case TagK:
146 f.K = elem(tg, 1)
147 case TagX:
148 f.X = elem(tg, 1)
149 case TagServer:
150 f.Server = elem(tg, 1)
151 case TagMint:
152 f.Mints = push(f.Mints, elem(tg, 1))
153 case TagRetailer:
154 f.Retailers = push(f.Retailers, elem(tg, 1))
155 case TagExpiration:
156 f.Expiration = elem(tg, 1)
157 case TagCatalog:
158 // On a filter, catalog carries covered asset addresses; the same
159 // tag name on an edition carries a release catalog number. The
160 // kind disambiguates.
161 for j := int32(1); j < int32(len(tg)); j++ {
162 f.Catalog = push(f.Catalog, tg[j])
163 }
164 default:
165 f.Extra = appendTag(f.Extra, tg)
166 }
167 }
168 return
169 }
170
171 func (f *AccessFilter) Validate() (err error) {
172 if f == nil {
173 err = errorf.E("nil access filter")
174 return
175 }
176 if _, terr := ParseFilterD(f.D); terr != nil {
177 err = terr
178 return
179 }
180 if f.Filter == "" {
181 err = errorf.E("access filter: no filter tag")
182 return
183 }
184 if f.Filter != FilterBloom {
185 // An unknown structure must fail closed at the host, which needs the
186 // value it saw; validation only says it is not one we can read.
187 err = errorf.E("access filter: unknown filter structure %q", f.Filter)
188 return
189 }
190 if !IsHex64(f.X) {
191 err = errorf.E("access filter: blob hash %q is not 64 hex characters", f.X)
192 return
193 }
194 if f.Fp == "" {
195 err = errorf.E("access filter: no false-positive rate")
196 return
197 }
198 if f.N == "" {
199 err = errorf.E("access filter: no element count")
200 return
201 }
202 if f.K == "" {
203 err = errorf.E("access filter: no hash-function count")
204 return
205 }
206 for i := range f.Retailers {
207 if !IsHex64(f.Retailers[i]) {
208 err = errorf.E("access filter: retailer %d pubkey is not 64 hex characters", i)
209 return
210 }
211 }
212 return
213 }
214
215 // Warnings reports the subscription coverage gap the spec leaves open: a sub
216 // filter must carry catalog tags or resolve to an addressable list.
217 func (f *AccessFilter) Warnings() (w []string) {
218 tier, err := ParseFilterD(f.D)
219 if err != nil {
220 return
221 }
222 if tier.Tier == TierSub && len(f.Catalog) == 0 {
223 w = push(w, "subscription filter with no catalog coverage")
224 }
225 if f.Expiration == "" {
226 w = push(w, "no expiration: the filter is permanent")
227 }
228 return
229 }
230
231 // PurchaseOrder is the payload of a kind 3222 event: a buyer's order carrying
232 // intent and payment proof in one message. It is never published - it exists
233 // only inside a NIP-59 wrap addressed to the seller.
234 type PurchaseOrder struct {
235 Asset *Address
236 Seller string
237 Tier string
238 Price *Price
239 Payment string // cashu or lightning
240 Mint string // cashu leg
241 Token string // cashuB..., P2PK-locked to the seller
242 Channel string // subscription orders
243 Period string // subscription orders
244 Content string
245 Extra [][]string
246 }
247
248 func (o *PurchaseOrder) Tags() (tags [][]string) {
249 tags = appendTag(nil, o.Asset.Tag())
250 tags = appendKV(tags, TagP, o.Seller)
251 tags = appendKV(tags, TagTier, o.Tier)
252 tags = appendTag(tags, o.Price.Tag())
253 tags = appendKV(tags, TagPayment, o.Payment)
254 tags = appendKV(tags, TagMint, o.Mint)
255 tags = appendKV(tags, TagToken, o.Token)
256 tags = appendKV(tags, TagChannel, o.Channel)
257 tags = appendKV(tags, TagPeriod, o.Period)
258 tags = appendTags(tags, o.Extra)
259 return
260 }
261
262 func ParsePurchaseOrder(tags [][]string, content string) (o *PurchaseOrder, err error) {
263 o = &PurchaseOrder{Content: content}
264 for i := range tags {
265 tg := tags[i]
266 if len(tg) < 1 {
267 continue
268 }
269 switch tg[0] {
270 case TagA:
271 a, aerr := AddressFromTag(tg)
272 if aerr != nil {
273 err = aerr
274 return
275 }
276 o.Asset = a
277 case TagP:
278 o.Seller = elem(tg, 1)
279 case TagTier:
280 o.Tier = elem(tg, 1)
281 case TagPrice:
282 o.Price = parsePrice(tg)
283 case TagPayment:
284 o.Payment = elem(tg, 1)
285 case TagMint:
286 o.Mint = elem(tg, 1)
287 case TagToken:
288 o.Token = elem(tg, 1)
289 case TagChannel:
290 o.Channel = elem(tg, 1)
291 case TagPeriod:
292 o.Period = elem(tg, 1)
293 default:
294 o.Extra = appendTag(o.Extra, tg)
295 }
296 }
297 return
298 }
299
300 func (o *PurchaseOrder) Validate() (err error) {
301 if o == nil {
302 err = errorf.E("nil purchase order")
303 return
304 }
305 if o.Asset == nil {
306 err = errorf.E("purchase order: no asset address")
307 return
308 }
309 if !IsHex64(o.Seller) {
310 err = errorf.E("purchase order: seller pubkey is not 64 hex characters")
311 return
312 }
313 if o.Tier != "" && !AccessTier(o.Tier) {
314 err = errorf.E("purchase order: %q is not a tier", o.Tier)
315 return
316 }
317 switch o.Payment {
318 case "":
319 err = errorf.E("purchase order: no payment leg")
320 return
321 case PaymentCashu:
322 if o.Token == "" || o.Mint == "" {
323 err = errorf.E("purchase order: a cashu leg needs a mint and a token")
324 return
325 }
326 case PaymentLightning:
327 // The proof is a NIP-57 zap receipt found separately; the order
328 // itself carries only the leg name.
329 }
330 if o.Tier == TierSub && (o.Channel == "" || o.Period == "") {
331 err = errorf.E("purchase order: a subscription needs a channel and a period")
332 return
333 }
334 return
335 }
336
337 // DeliveryReceipt is the payload of a kind 32218 event: the host-signed deed
338 // for one serve. It is never published; one wrap goes to the buyer and one to
339 // the publisher, and the publisher's archive is the durable record.
340 type DeliveryReceipt struct {
341 Asset *Address
342 Buyer string
343 Blob string
344 Server string
345 Mode string // download or stream
346 Start string
347 End string
348 Bytes string
349 AmountSats string
350 Paid []string // token hashes spent at this serve
351 Content string
352 Extra [][]string
353 }
354
355 func (r *DeliveryReceipt) Tags() (tags [][]string) {
356 tags = appendTag(nil, r.Asset.Tag())
357 tags = appendKV(tags, TagP, r.Buyer)
358 tags = appendKV(tags, TagX, r.Blob)
359 tags = appendKV(tags, TagServer, r.Server)
360 tags = appendKV(tags, TagMode, r.Mode)
361 tags = appendKV(tags, TagStart, r.Start)
362 tags = appendKV(tags, TagEnd, r.End)
363 tags = appendKV(tags, TagBytes, r.Bytes)
364 tags = appendKV(tags, TagAmountSats, r.AmountSats)
365 for i := range r.Paid {
366 tags = appendKV(tags, TagPaid, r.Paid[i])
367 }
368 tags = appendTags(tags, r.Extra)
369 return
370 }
371
372 func ParseDeliveryReceipt(tags [][]string, content string) (r *DeliveryReceipt, err error) {
373 r = &DeliveryReceipt{Content: content}
374 for i := range tags {
375 tg := tags[i]
376 if len(tg) < 1 {
377 continue
378 }
379 switch tg[0] {
380 case TagA:
381 a, aerr := AddressFromTag(tg)
382 if aerr != nil {
383 err = aerr
384 return
385 }
386 r.Asset = a
387 case TagP:
388 r.Buyer = elem(tg, 1)
389 case TagX:
390 r.Blob = elem(tg, 1)
391 case TagServer:
392 r.Server = elem(tg, 1)
393 case TagMode:
394 r.Mode = elem(tg, 1)
395 case TagStart:
396 r.Start = elem(tg, 1)
397 case TagEnd:
398 r.End = elem(tg, 1)
399 case TagBytes:
400 r.Bytes = elem(tg, 1)
401 case TagAmountSats:
402 r.AmountSats = elem(tg, 1)
403 case TagPaid:
404 r.Paid = push(r.Paid, elem(tg, 1))
405 default:
406 r.Extra = appendTag(r.Extra, tg)
407 }
408 }
409 return
410 }
411
412 func (r *DeliveryReceipt) Validate() (err error) {
413 if r == nil {
414 err = errorf.E("nil delivery receipt")
415 return
416 }
417 if r.Asset == nil {
418 err = errorf.E("delivery receipt: no asset address")
419 return
420 }
421 if !IsHex64(r.Buyer) {
422 err = errorf.E("delivery receipt: buyer pubkey is not 64 hex characters")
423 return
424 }
425 if !IsHex64(r.Blob) {
426 err = errorf.E("delivery receipt: blob hash is not 64 hex characters")
427 return
428 }
429 switch r.Mode {
430 case ModeDownload, ModeStream:
431 case "":
432 err = errorf.E("delivery receipt: no mode")
433 return
434 default:
435 err = errorf.E("delivery receipt: %q is not a mode", r.Mode)
436 return
437 }
438 if r.AmountSats == "" {
439 err = errorf.E("delivery receipt: no amount")
440 return
441 }
442 return
443 }
444
445 // TokenHashesUsed reports the token hashes a receipt claims, so a host can
446 // link a deed to a payment without the token itself ever being published.
447 func (r *DeliveryReceipt) TokenHashesUsed() (n int32) {
448 return int32(len(r.Paid))
449 }
450
451 // HostAggregate is the payload of a kind 32219 event: a serving host's signed
452 // totals for one asset and period. Immutable: a correction is a new event with
453 // a revised period.
454 type HostAggregate struct {
455 Asset *Address
456 Start string
457 End string
458 Plays string
459 Downloads string
460 Bytes string
461 Sats string
462 Listeners string
463 Approx bool // listeners comes from a probabilistic filter and is an estimate
464 Content string
465 Extra [][]string
466 }
467
468 func (a *HostAggregate) Tags() (tags [][]string) {
469 tags = appendTag(nil, a.Asset.Tag())
470 tags = appendKV(tags, TagStart, a.Start)
471 tags = appendKV(tags, TagEnd, a.End)
472 tags = appendKV(tags, TagPlays, a.Plays)
473 tags = appendKV(tags, TagDownloads, a.Downloads)
474 tags = appendKV(tags, TagBytes, a.Bytes)
475 tags = appendKV(tags, TagSats, a.Sats)
476 if a.Approx {
477 t := []string{:3}
478 t[0] = TagListeners
479 t[1] = a.Listeners
480 t[2] = Approx
481 tags = appendTag(tags, t)
482 } else {
483 tags = appendKV(tags, TagListeners, a.Listeners)
484 }
485 tags = appendTags(tags, a.Extra)
486 return
487 }
488
489 func ParseHostAggregate(tags [][]string, content string) (a *HostAggregate, err error) {
490 a = &HostAggregate{Content: content}
491 for i := range tags {
492 tg := tags[i]
493 if len(tg) < 1 {
494 continue
495 }
496 switch tg[0] {
497 case TagA:
498 addr, aerr := AddressFromTag(tg)
499 if aerr != nil {
500 err = aerr
501 return
502 }
503 a.Asset = addr
504 case TagStart:
505 a.Start = elem(tg, 1)
506 case TagEnd:
507 a.End = elem(tg, 1)
508 case TagPlays:
509 a.Plays = elem(tg, 1)
510 case TagDownloads:
511 a.Downloads = elem(tg, 1)
512 case TagBytes:
513 a.Bytes = elem(tg, 1)
514 case TagSats:
515 a.Sats = elem(tg, 1)
516 case TagListeners:
517 a.Listeners = elem(tg, 1)
518 if elem(tg, 2) == Approx {
519 a.Approx = true
520 }
521 default:
522 a.Extra = appendTag(a.Extra, tg)
523 }
524 }
525 return
526 }
527
528 func (a *HostAggregate) Validate() (err error) {
529 if a == nil {
530 err = errorf.E("nil host aggregate")
531 return
532 }
533 if a.Asset == nil {
534 err = errorf.E("host aggregate: no asset address")
535 return
536 }
537 if a.Start == "" || a.End == "" {
538 err = errorf.E("host aggregate: no period")
539 return
540 }
541 if a.Listeners != "" && !a.Approx {
542 // The spec says the estimate must say so; a bare count reads as exact.
543 err = errorf.E("host aggregate: listeners must be marked approx")
544 return
545 }
546 return
547 }
548
549 // AggregateRef is an `e` tag on a publisher rollup: the referenced host
550 // aggregate, plus the host that signed it. The relay slot is written empty so
551 // the host pubkey stays at index 3.
552 type AggregateRef struct {
553 ID string
554 Host string
555 }
556
557 func (r *AggregateRef) Tag() (t []string) {
558 if r == nil {
559 return nil
560 }
561 t = []string{:4}
562 t[0] = TagE
563 t[1] = r.ID
564 t[2] = ""
565 t[3] = r.Host
566 return
567 }
568
569 // PublisherRollup is the payload of a kind 32220 event: the publisher's signed
570 // pointer-set over host aggregates plus its own receipt-archive totals. The
571 // leaves stay host-signed, so a rollup never restates a host's numbers.
572 type PublisherRollup struct {
573 Asset *Address
574 Start string
575 End string
576 Aggregates []AggregateRef
577 Receipts string
578 Plays string
579 Downloads string
580 Bytes string
581 Sats string
582 Content string
583 Extra [][]string
584 }
585
586 func (r *PublisherRollup) Tags() (tags [][]string) {
587 tags = appendTag(nil, r.Asset.Tag())
588 tags = appendKV(tags, TagStart, r.Start)
589 tags = appendKV(tags, TagEnd, r.End)
590 for i := range r.Aggregates {
591 tags = appendTag(tags, r.Aggregates[i].Tag())
592 }
593 tags = appendKV(tags, TagReceipts, r.Receipts)
594 tags = appendKV(tags, TagPlays, r.Plays)
595 tags = appendKV(tags, TagDownloads, r.Downloads)
596 tags = appendKV(tags, TagBytes, r.Bytes)
597 tags = appendKV(tags, TagSats, r.Sats)
598 tags = appendTags(tags, r.Extra)
599 return
600 }
601
602 func ParsePublisherRollup(tags [][]string, content string) (r *PublisherRollup, err error) {
603 r = &PublisherRollup{Content: content}
604 for i := range tags {
605 tg := tags[i]
606 if len(tg) < 1 {
607 continue
608 }
609 switch tg[0] {
610 case TagA:
611 a, aerr := AddressFromTag(tg)
612 if aerr != nil {
613 err = aerr
614 return
615 }
616 r.Asset = a
617 case TagStart:
618 r.Start = elem(tg, 1)
619 case TagEnd:
620 r.End = elem(tg, 1)
621 case TagE:
622 r.Aggregates = push(r.Aggregates, AggregateRef{ID: elem(tg, 1), Host: elem(tg, 3)})
623 case TagReceipts:
624 r.Receipts = elem(tg, 1)
625 case TagPlays:
626 r.Plays = elem(tg, 1)
627 case TagDownloads:
628 r.Downloads = elem(tg, 1)
629 case TagBytes:
630 r.Bytes = elem(tg, 1)
631 case TagSats:
632 r.Sats = elem(tg, 1)
633 default:
634 r.Extra = appendTag(r.Extra, tg)
635 }
636 }
637 return
638 }
639
640 func (r *PublisherRollup) Validate() (err error) {
641 if r == nil {
642 err = errorf.E("nil publisher rollup")
643 return
644 }
645 if r.Asset == nil {
646 err = errorf.E("publisher rollup: no asset address")
647 return
648 }
649 if r.Start == "" || r.End == "" {
650 err = errorf.E("publisher rollup: no period")
651 return
652 }
653 for i := range r.Aggregates {
654 if !IsHex64(r.Aggregates[i].ID) {
655 err = errorf.E("publisher rollup: aggregate %d id is not 64 hex characters", i)
656 return
657 }
658 if !IsHex64(r.Aggregates[i].Host) {
659 err = errorf.E("publisher rollup: aggregate %d host pubkey is not 64 hex characters", i)
660 return
661 }
662 }
663 return
664 }
665
666 // indexByte returns the index of the first c in s, or -1.
667 func indexByte(s string, c byte) (i int32) {
668 for j := int32(0); j < int32(len(s)); j++ {
669 if s[j] == c {
670 return j
671 }
672 }
673 return -1
674 }
675