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 }