go-nostr/nip60/history.go
2025-02-04 10:25:13 -03:00

162 lines
3.1 KiB
Go

package nip60
import (
"context"
"encoding/json"
"fmt"
"strconv"
"github.com/nbd-wtf/go-nostr"
)
type HistoryEntry struct {
In bool // in = received, out = sent
Amount uint64
TokenReferences []TokenRef
createdAt nostr.Timestamp
event *nostr.Event
}
type TokenRef struct {
EventID string
Created bool
IsNutzap bool
}
func (h HistoryEntry) toEvent(ctx context.Context, kr nostr.Keyer, evt *nostr.Event) error {
pk, err := kr.GetPublicKey(ctx)
if err != nil {
return err
}
dir := "in"
if !h.In {
dir = "out"
}
evt.CreatedAt = h.createdAt
evt.Kind = 7376
evt.Tags = nostr.Tags{}
encryptedTags := nostr.Tags{
nostr.Tag{"direction", dir},
nostr.Tag{"amount", strconv.FormatUint(uint64(h.Amount), 10)},
}
for _, tf := range h.TokenReferences {
if tf.IsNutzap {
evt.Tags = append(evt.Tags, nostr.Tag{"e", tf.EventID, "", "redeemed"})
continue
}
marker := "destroyed"
if tf.Created {
marker = "created"
}
encryptedTags = append(encryptedTags, nostr.Tag{"e", tf.EventID, "", marker})
}
jsonb, _ := json.Marshal(encryptedTags)
evt.Content, err = kr.Encrypt(
ctx,
string(jsonb),
pk,
)
if err != nil {
return err
}
err = kr.SignEvent(ctx, evt)
if err != nil {
return err
}
return nil
}
func (h *HistoryEntry) parse(ctx context.Context, kr nostr.Keyer, evt *nostr.Event) error {
h.event = evt
h.createdAt = evt.CreatedAt
h.TokenReferences = make([]TokenRef, 0, 3)
pk, err := kr.GetPublicKey(ctx)
if err != nil {
return err
}
// event tags and encrypted tags are mixed together
jsonb, err := kr.Decrypt(ctx, evt.Content, pk)
if err != nil {
return err
}
var tags nostr.Tags
if len(jsonb) > 0 {
tags = make(nostr.Tags, 0, 7)
if err := json.Unmarshal([]byte(jsonb), &tags); err != nil {
return err
}
tags = append(tags, evt.Tags...)
}
missingDirection := true
for _, tag := range tags {
if len(tag) < 2 {
continue
}
switch tag[0] {
case "direction":
missingDirection = false
if tag[1] == "in" {
h.In = true
} else if tag[1] == "out" {
h.In = false
} else {
return fmt.Errorf("unexpected 'direction' tag %s", tag[1])
}
case "amount":
if len(tag) < 2 {
return fmt.Errorf("'amount' tag must have at least 2 items")
}
v, err := strconv.ParseUint(tag[1], 10, 64)
if err != nil {
return fmt.Errorf("invalid 'amount' %s: %w", tag[1], err)
}
h.Amount = v
case "e":
if len(tag) < 4 {
return fmt.Errorf("'e' tag must have at least 4 items")
}
if !nostr.IsValid32ByteHex(tag[1]) {
return fmt.Errorf("'e' tag has invalid event id %s", tag[1])
}
tf := TokenRef{EventID: tag[1]}
switch tag[3] {
case "created":
tf.Created = true
case "destroyed":
tf.Created = false
case "redeemed":
tf.IsNutzap = true
tf.Created = true
default:
return fmt.Errorf("unsupported 'e' token marker: %s", tag[3])
}
h.TokenReferences = append(h.TokenReferences, tf)
}
}
if h.Amount == 0 {
return fmt.Errorf("missing 'amount' tag")
}
if missingDirection {
return fmt.Errorf("missing 'direction' tag")
}
return nil
}