well-goknown/alby/utils.go

278 lines
7 KiB
Go

package alby
import (
"context"
"encoding/json"
"fmt"
"sync"
"time"
"git.devvul.com/asara/gologger"
"git.devvul.com/asara/well-goknown/config"
"github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip04"
)
// check if event is valid
func checkEvent(n string) bool {
var zapEvent ZapEvent
l := gologger.Get(config.GetConfig().LogLevel).With().Caller().Logger()
err := json.Unmarshal([]byte(n), &zapEvent)
if err != nil {
l.Debug().Msgf("unable to unmarshal nwc value: %s", err.Error())
return false
}
if err != nil {
l.Debug().Msgf("unable to read tags from nostr request: %s", err.Error())
return false
}
evt := nostr.Event{
ID: zapEvent.Id,
PubKey: zapEvent.Pubkey,
CreatedAt: zapEvent.CreatedAt,
Kind: zapEvent.Kind,
Tags: zapEvent.Tags,
Content: zapEvent.Content,
Sig: zapEvent.Signature,
}
ok, err := evt.CheckSignature()
if !ok {
l.Debug().Msgf("event is invalid", err.Error())
return false
}
return true
}
// background task to return a receipt when the payment is paid
func watchForReceipt(nEvent string, secret NWCSecret, invoice string) {
var zapEvent ZapEvent
l := gologger.Get(config.GetConfig().LogLevel).With().Caller().Logger()
_, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
ok := checkEvent(nEvent)
if !ok {
l.Debug().Msgf("nostr event is not valid")
return
}
err := json.Unmarshal([]byte(nEvent), &zapEvent)
if err != nil {
l.Debug().Msgf("unable to unmarshal nwc value: %s", err.Error())
return
}
ticker := time.NewTicker(30 * time.Second)
quit := make(chan struct{})
go func() {
defer ticker.Stop()
for {
select {
case <-quit:
return
case _ = <-ticker.C:
paid, failed, result := checkInvoicePaid(invoice, secret, nEvent)
if failed {
close(quit)
return
}
if paid {
sendReceipt(secret, result, nEvent)
close(quit)
return
}
}
}
defer close(quit)
}()
}
func checkInvoicePaid(checkInvoice string, secret NWCSecret, nEvent string) (bool, bool, LookupInvoiceResponse) {
l := gologger.Get(config.GetConfig().LogLevel).With().Caller().Logger()
invoiceParams := LookupInvoiceParams{
Invoice: checkInvoice,
}
invoice := LookupInvoice{
Method: "lookup_invoice",
Params: invoiceParams,
}
invoiceJson, err := json.Marshal(invoice)
if err != nil {
l.Debug().Msgf("unable to marshal invoice: %s", err.Error())
return false, true, LookupInvoiceResponse{}
}
// generate nip-04 shared secret
sharedSecret, err := nip04.ComputeSharedSecret(secret.AppPubkey, secret.Secret)
if err != nil {
l.Debug().Msgf("unable to marshal invoice: %s", err.Error())
return false, true, LookupInvoiceResponse{}
}
// create the encrypted content payload
encryptedContent, err := nip04.Encrypt(string(invoiceJson), sharedSecret)
if err != nil {
l.Debug().Msgf("unable to marshal invoice: %s", err.Error())
return false, true, LookupInvoiceResponse{}
}
recipient := nostr.Tag{"p", secret.AppPubkey}
nwcEv := nostr.Event{
PubKey: secret.AppPubkey,
CreatedAt: nostr.Now(),
Kind: nostr.KindNWCWalletRequest,
Tags: nostr.Tags{recipient},
Content: encryptedContent,
}
// sign the message with the app token
nwcEv.Sign(secret.Secret)
var filters nostr.Filters
t := make(map[string][]string)
t["e"] = []string{nwcEv.GetID()}
filters = []nostr.Filter{
{
Kinds: []int{
nostr.KindNWCWalletResponse,
},
Tags: t,
},
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
relay, err := nostr.RelayConnect(ctx, secret.Relay)
subCtx, subCancel := context.WithTimeout(context.Background(), 5*time.Second)
defer subCancel()
// subscribe to the filter
sub, err := relay.Subscribe(subCtx, filters)
if err != nil {
l.Debug().Msgf("unable to connect to relay: %s", err.Error())
return false, false, LookupInvoiceResponse{}
}
var wg sync.WaitGroup
wg.Add(1)
// watch for the invoice
evs := make([]nostr.Event, 0)
go func() {
defer wg.Done()
for {
select {
case ev, ok := <-sub.Events:
if !ok {
l.Debug().Msgf("subscription events channel is closed")
return
}
if ev.Kind != 0 {
evs = append(evs, *ev)
}
if len(evs) > 0 {
return
}
case <-sub.EndOfStoredEvents:
l.Trace().Msgf("end of stored events received")
case <-ctx.Done():
l.Debug().Msgf("subscription context cancelled or done: %v", ctx.Err())
return
}
}
}()
// publish the invoice request
if err := relay.Publish(ctx, nwcEv); err != nil {
l.Debug().Msgf("unable to publish event: %s", err.Error())
return false, false, LookupInvoiceResponse{}
}
// wait for the invoice to get returned
wg.Wait()
// decrypt the invoice
response, err := nip04.Decrypt(evs[0].Content, sharedSecret)
resStruct := LookupInvoiceResponse{}
err = json.Unmarshal([]byte(response), &resStruct)
if err != nil {
l.Debug().Msgf("unable to unmarshal invoice response: %s", err.Error())
return false, true, LookupInvoiceResponse{}
}
if settled := resStruct.Result.isSettled(); settled {
return true, false, resStruct
}
if expired := resStruct.Result.isExpired(); expired {
return false, true, LookupInvoiceResponse{}
}
return false, false, LookupInvoiceResponse{}
}
func sendReceipt(secret NWCSecret, result LookupInvoiceResponse, nEvent string) {
l := gologger.Get(config.GetConfig().LogLevel).With().Caller().Logger()
var zapRequestEvent ZapEvent
err := json.Unmarshal([]byte(nEvent), &zapRequestEvent)
if err != nil {
return
}
zapReceipt := nostr.Event{
PubKey: secret.ClientPubkey,
CreatedAt: result.Result.SettledAt,
Kind: nostr.KindNWCWalletResponse,
Tags: zapRequestEvent.Tags,
Content: "",
}
// add context to zapReceipt
sender := nostr.Tag{"P", zapRequestEvent.Pubkey}
bolt11 := nostr.Tag{"bolt11", result.Result.Invoice}
preimage := nostr.Tag{"preimage", result.Result.Preimage}
description := nostr.Tag{"description", nEvent}
zapReceipt.Tags = zapReceipt.Tags.AppendUnique(sender)
zapReceipt.Tags = zapReceipt.Tags.AppendUnique(bolt11)
zapReceipt.Tags = zapReceipt.Tags.AppendUnique(preimage)
zapReceipt.Tags = zapReceipt.Tags.AppendUnique(description)
// remove unneeded values from tags
zapReceipt.Tags = zapReceipt.Tags.FilterOut([]string{"relays"})
zapReceipt.Tags = zapReceipt.Tags.FilterOut([]string{"alt"})
// sign the receipt
zapReceipt.Sign(secret.Secret)
// send it to the listed relays
ctx := context.Background()
relayTag := zapRequestEvent.Tags.GetFirst([]string{"relays"})
var report []string
for idx, url := range *relayTag {
if idx == 0 {
continue
}
relay, err := nostr.RelayConnect(ctx, url)
if err != nil {
report = append(report, fmt.Sprintf("error: unable to connect to relay (%s): %s", url, err.Error()))
return
}
if err := relay.Publish(ctx, zapReceipt); err != nil {
report = append(report, fmt.Sprintf("error: unable to connect to relay (%s): %s", url, err.Error()))
return
}
report = append(report, fmt.Sprintf("success: sent receipt to %s", url))
}
l.Debug().Msgf("receipt report: %v", report)
return
}