278 lines
7 KiB
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
|
|
}
|