diff --git a/sdk/replaceable_loader.go b/sdk/replaceable_loader.go index 382ae4c..ed94fb6 100644 --- a/sdk/replaceable_loader.go +++ b/sdk/replaceable_loader.go @@ -27,13 +27,14 @@ const ( kind_10006 replaceableIndex = 8 kind_10007 replaceableIndex = 9 kind_10015 replaceableIndex = 10 - kind_10030 replaceableIndex = 11 + kind_10019 replaceableIndex = 11 + kind_10030 replaceableIndex = 12 ) type EventResult dataloader.Result[*nostr.Event] func (sys *System) initializeReplaceableDataloaders() { - sys.replaceableLoaders = make([]*dataloader.Loader[nostr.PubKey, nostr.Event], 12) + sys.replaceableLoaders = make([]*dataloader.Loader[nostr.PubKey, nostr.Event], 13) sys.replaceableLoaders[kind_0] = sys.createReplaceableDataloader(0) sys.replaceableLoaders[kind_3] = sys.createReplaceableDataloader(3) sys.replaceableLoaders[kind_10000] = sys.createReplaceableDataloader(10000) @@ -45,6 +46,7 @@ func (sys *System) initializeReplaceableDataloaders() { sys.replaceableLoaders[kind_10006] = sys.createReplaceableDataloader(10006) sys.replaceableLoaders[kind_10007] = sys.createReplaceableDataloader(10007) sys.replaceableLoaders[kind_10015] = sys.createReplaceableDataloader(10015) + sys.replaceableLoaders[kind_10019] = sys.createReplaceableDataloader(10019) sys.replaceableLoaders[kind_10030] = sys.createReplaceableDataloader(10030) } diff --git a/sdk/system.go b/sdk/system.go index 0aebfb9..1b282fd 100644 --- a/sdk/system.go +++ b/sdk/system.go @@ -43,6 +43,7 @@ type System struct { TopicSetsCache cache.Cache32[GenericSets[string, Topic]] ZapProviderCache cache.Cache32[nostr.PubKey] MintKeysCache cache.Cache32[map[uint64]*btcec.PublicKey] + NutZapInfoCache cache.Cache32[NutZapInfo] Hints hints.HintsDB Pool *nostr.Pool RelayListRelays *RelayStream @@ -140,6 +141,9 @@ func NewSystem() *System { if sys.MintKeysCache == nil { sys.MintKeysCache = cache_memory.New[map[uint64]*btcec.PublicKey](8000) } + if sys.NutZapInfoCache == nil { + sys.NutZapInfoCache = cache_memory.New[NutZapInfo](8000) + } if sys.Store == nil { sys.Store = &nullstore.NullStore{} diff --git a/sdk/zap.go b/sdk/zap.go index 614860b..1856f11 100644 --- a/sdk/zap.go +++ b/sdk/zap.go @@ -11,10 +11,20 @@ import ( "fiatjaf.com/nostr" "fiatjaf.com/nostr/nip60" "fiatjaf.com/nostr/nip60/client" + "fiatjaf.com/nostr/nip61" "github.com/btcsuite/btcd/btcec/v2" "github.com/tidwall/gjson" ) +// NutZapInfo represents user nut zap information from kind 10019 events. +// contains both the raw event and parsed info fields. +type NutZapInfo struct { + PubKey nostr.PubKey `json:"-"` // must always be set otherwise things will break + Event *nostr.Event `json:"-"` // may be empty if a nut zap info event wasn't found + + nip61.Info +} + // FetchZapProvider fetches the zap provider public key for a given user from their profile metadata. // It uses a cache to avoid repeated fetches. If no zap provider is set in the profile, returns an empty PubKey. func (sys *System) FetchZapProvider(ctx context.Context, pk nostr.PubKey) nostr.PubKey { @@ -52,6 +62,84 @@ func (sys *System) FetchZapProvider(ctx context.Context, pk nostr.PubKey) nostr. return nostr.ZeroPK } +// FetchNutZapInfo fetches nut zap info for a given user from the local cache, or from the local store, +// or, failing these, from the target user's defined outbox relays -- then caches the result. +// always returns a NutZapInfo, even if no info was found (in which case only the PubKey field is set). +func (sys *System) FetchNutZapInfo(ctx context.Context, pubkey nostr.PubKey) NutZapInfo { + if v, ok := sys.NutZapInfoCache.Get(pubkey); ok { + return v + } + + for evt := range sys.Store.QueryEvents(nostr.Filter{Kinds: []nostr.Kind{10019}, Authors: []nostr.PubKey{pubkey}}, 1) { + // ok, we found something locally + nzi, err := ParseNutZapInfo(evt) + if err != nil { + break + } + + // but if we haven't tried fetching from the network recently we should do it + lastFetchKey := makeLastFetchKey(10019, pubkey) + lastFetchData, _ := sys.KVStore.Get(lastFetchKey) + if lastFetchData == nil || nostr.Now()-decodeTimestamp(lastFetchData) > 7*24*60*60 { + newNZI := sys.tryFetchNutZapInfoFromNetwork(ctx, pubkey) + if newNZI != nil && newNZI.Event.CreatedAt > nzi.Event.CreatedAt { + nzi = *newNZI + } + + // even if we didn't find anything register this because we tried + // (and we still have the previous event in our local store) + sys.KVStore.Set(lastFetchKey, encodeTimestamp(nostr.Now())) + } + + // and finally save this to cache + sys.NutZapInfoCache.SetWithTTL(pubkey, nzi, time.Hour*6) + + return nzi + } + + var nzi NutZapInfo + nzi.PubKey = pubkey + if newNZI := sys.tryFetchNutZapInfoFromNetwork(ctx, pubkey); newNZI != nil { + nzi = *newNZI + + // we'll only save this if we got something which means we found at least one event + lastFetchKey := makeLastFetchKey(10019, pubkey) + sys.KVStore.Set(lastFetchKey, encodeTimestamp(nostr.Now())) + } + + // save cache even if we didn't get anything + sys.NutZapInfoCache.SetWithTTL(pubkey, nzi, time.Hour*6) + + return nzi +} + +func (sys *System) tryFetchNutZapInfoFromNetwork(ctx context.Context, pubkey nostr.PubKey) *NutZapInfo { + evt, err := sys.replaceableLoaders[kind_10019].Load(ctx, pubkey) + if err != nil { + return nil + } + + nzi, err := ParseNutZapInfo(evt) + if err != nil { + return nil + } + + sys.Publisher.Publish(ctx, evt) + sys.NutZapInfoCache.SetWithTTL(pubkey, nzi, time.Hour*6) + return &nzi +} + +// ParseNutZapInfo parses a kind 10019 event into a NutZapInfo struct. +// returns an error if the event is not kind 10019 or if parsing fails. +func ParseNutZapInfo(event nostr.Event) (nzi NutZapInfo, err error) { + err = nzi.Info.ParseEvent(event) + + nzi.PubKey = event.PubKey + nzi.Event = &event + + return nzi, err +} + // FetchMintKeys fetches the active keyset from the given mint URL and parses the keys. // It uses a cache to avoid repeated fetches. func (sys *System) FetchMintKeys(ctx context.Context, mintURL string) (map[uint64]*btcec.PublicKey, error) {