Files
nostdress/lnurl.go
T
2023-03-02 15:13:48 +01:00

294 lines
9.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"github.com/fiatjaf/go-lnurl"
"github.com/gorilla/mux"
"github.com/nbd-wtf/go-nostr"
decodepay "github.com/nbd-wtf/ln-decodepay"
)
var allowNostr bool = false
var nostrPrivkeyHex string = ""
var nostrPubkey string = ""
var minSendable int = 1000
var maxSendable int = 1000000000
var CommentAllowed int = 500
type LNURLPayParamsCustom struct {
lnurl.LNURLResponse
Callback string `json:"callback"`
Tag string `json:"tag"`
MaxSendable int64 `json:"maxSendable"`
MinSendable int64 `json:"minSendable"`
EncodedMetadata string `json:"metadata"`
CommentAllowed int64 `json:"commentAllowed"`
PayerData *lnurl.PayerDataSpec `json:"payerData,omitempty"`
AllowsNostr bool `json:"allowsNostr,omitempty"`
NostrPubKey string `json:"nostrPubkey,omitempty"`
Metadata lnurl.Metadata `json:"-"`
}
type LNURLPayValuesCustom struct {
lnurl.LNURLResponse
SuccessAction *lnurl.SuccessAction `json:"successAction"`
Routes interface{} `json:"routes"` // ignored
PR string `json:"pr"`
Disposable *bool `json:"disposable,omitempty"`
ParsedInvoice decodepay.Bolt11 `json:"-"`
PayerDataJSON string `json:"-"`
nip57Receipt nostr.Event `json:"nip57Receipt"`
nip57ReceiptRelays []string `json:"nip57ReceiptRelays"`
}
func handleLNURL(w http.ResponseWriter, r *http.Request) {
var err error
var response interface{}
username := mux.Vars(r)["user"]
domains := getDomains(s.Domain)
domain := ""
if len(domains) == 1 {
domain = domains[0]
} else {
hostname := r.URL.Host
if hostname == "" {
hostname = r.Host
}
for _, one := range getDomains(s.Domain) {
if strings.Contains(hostname, one) {
domain = one
break
}
}
if domain == "" {
json.NewEncoder(w).Encode(lnurl.ErrorResponse("incorrect domain"))
return
}
}
params, err := GetName(username, domain)
if err != nil {
log.Error().Err(err).Str("name", username).Str("domain", domain).Msg("failed to get name")
json.NewEncoder(w).Encode(lnurl.ErrorResponse(fmt.Sprintf(
"failed to get name %s@%s", username, domain)))
return
}
log.Info().Str("username", username).Str("domain", domain).Msg("got lnurl request")
if amount := r.URL.Query().Get("amount"); amount == "" {
// convert configured sendable amounts to integer
minSendable, err := strconv.ParseInt(params.MinSendable, 10, 64)
// set defaults
if err != nil {
minSendable = 1000
}
maxSendable, err := strconv.ParseInt(params.MaxSendable, 10, 64)
if err != nil {
maxSendable = 1000000000
}
// if a nostr private nsec key is set, set nostr nip57 flags
if len(s.NostrPrivateKey) > 0 {
//allows users to use nsec keys, work with hex internally.
//This can be any private key, not necessarily from the user.
nostrPrivkeyHex = DecodeBench32(s.NostrPrivateKey)
allowNostr = true
pk := nostrPrivkeyHex
pub, _ := nostr.GetPublicKey(pk)
nostrPubkey = pub
}
json.NewEncoder(w).Encode(LNURLPayParamsCustom{
LNURLResponse: lnurl.LNURLResponse{Status: "OK"},
Callback: fmt.Sprintf("https://%s/.well-known/lnurlp/%s", domain, username),
MinSendable: minSendable,
MaxSendable: maxSendable,
EncodedMetadata: metaData(params).Encode(),
CommentAllowed: int64(CommentAllowed),
Tag: "payRequest",
AllowsNostr: allowNostr,
NostrPubKey: nostrPubkey,
})
} else {
msat, err := strconv.Atoi(amount)
if err != nil {
json.NewEncoder(w).Encode(lnurl.ErrorResponse("amount is not integer"))
return
}
// nostr NIP-57
// the "nostr" query param has a zap request which is a nostr event
// that specifies which nostr note has been zapped.
// here we check wheter its present, the event signature is valid
// and whether the event has the necessary tags that we need (p and relays are necessary, e is optional)
zapEventQuery := r.FormValue("nostr")
var zapEvent nostr.Event
if len(zapEventQuery) > 0 {
err = json.Unmarshal([]byte(zapEventQuery), &zapEvent)
if err != nil {
log.Error().Err(err).Str("[handleLnUrl] Couldn't parse nostr event: ", err.Error())
} else {
valid, err := zapEvent.CheckSignature()
if !valid || err != nil {
log.Error().Err(err).Str("[handleLnUrl] Nostr NIP-57 zap event signature invalid: ", err.Error())
return
}
if len(zapEvent.Tags) == 0 || zapEvent.Tags.GetFirst([]string{"p"}) == nil {
log.Error().Err(err).Str("[handleLnUrl] Nostr NIP-57 zap event validation error ", err.Error())
return
}
}
}
comment := r.FormValue("comment")
if len(comment) > CommentAllowed {
log.Error().Err(err).Str("[handleLnUrl] Comment is too long", err.Error())
return
}
// payer data
payerdata := r.FormValue("payerdata")
var payerData lnurl.PayerDataValues
if len(payerdata) > 0 {
err = json.Unmarshal([]byte(payerdata), &payerData)
if err != nil {
log.Error().Err(err).Str("[handleLnUrl] Couldn't parse payerdata", err.Error())
}
}
//we outsource the second part in a function, we should do this for the first one too.
response, err = serveLNURLpSecond(w, params, username, msat, comment, payerData, zapEvent)
var payvaluescustom = response.(LNURLPayValuesCustom)
//var payvalues = response.(*lnurl.LNURLPayValues)
if err != nil {
if response != nil {
// there is a valid error response
json.NewEncoder(w).Encode(response)
}
return
}
json.NewEncoder(w).Encode(lnurl.LNURLPayValues{
LNURLResponse: payvaluescustom.LNURLResponse,
PR: payvaluescustom.PR,
Routes: payvaluescustom.Routes,
SuccessAction: payvaluescustom.SuccessAction,
})
//err = json.NewEncoder(w).Encode(payvalues)
if err != nil {
json.NewEncoder(w).Encode(response)
return
}
if allowNostr {
go WaitForInvoicePaid(payvaluescustom, params)
}
}
}
func serveLNURLpSecond(w http.ResponseWriter, params *Params, username string, amount_msat int, comment string, payerData lnurl.PayerDataValues, zapEvent nostr.Event) (LNURLPayValuesCustom, error) {
log.Debug().Any("Serving invoice for user %s", username)
if amount_msat < minSendable || amount_msat > maxSendable {
// amount is not ok
return LNURLPayValuesCustom{
LNURLResponse: lnurl.LNURLResponse{
Status: "Error",
Reason: fmt.Sprintf("Amount out of bounds (min: %d sat, max: %d sat).", minSendable/1000, maxSendable/1000)},
}, fmt.Errorf("amount out of bounds")
}
// NIP57 ZAPs
// for nip57 use the nostr event as the descriptionHash
if zapEvent.Sig != "" {
// we calculate the descriptionHash here, create an invoice with it
// and store the invoice in the zap receipt later down the line
zapEventSerialized, err := json.Marshal(zapEvent)
zapEventSerializedStr = fmt.Sprintf("%s", zapEventSerialized)
if err != nil {
//log.Println(err)
return LNURLPayValuesCustom{
LNURLResponse: lnurl.LNURLResponse{
Status: "Error",
Reason: "Couldn't serialize zap event."},
}, err
}
// we extract the relays from the zap request
nip57ReceiptRelaysTags := zapEvent.Tags.GetFirst([]string{"relays"})
if len(fmt.Sprintf("%s", nip57ReceiptRelaysTags)) > 0 {
nip57ReceiptRelays = strings.Split(fmt.Sprintf("%s", nip57ReceiptRelaysTags), " ")
// this tirty method returns slice [ "[relays", "wss...", "wss...", "wss...]" ] we need to clean it up
if len(nip57ReceiptRelays) > 1 {
// remove the first entry
nip57ReceiptRelays = nip57ReceiptRelays[1:]
// clean up the last entry
len_last_entry := len(nip57ReceiptRelays[len(nip57ReceiptRelays)-1])
nip57ReceiptRelays[len(nip57ReceiptRelays)-1] = nip57ReceiptRelays[len(nip57ReceiptRelays)-1][:len_last_entry-1]
}
}
// calculate description hash from the serialized nostr event in makeinvoice.go
params.zapEventSerializedStr = zapEventSerializedStr
} else {
params.zapEventSerializedStr = ""
}
var response LNURLPayValuesCustom
invoice, err := makeInvoice(params, amount_msat, nil)
if err != nil {
err = fmt.Errorf("Couldn't create invoice: %v", err.Error())
response = LNURLPayValuesCustom{
LNURLResponse: lnurl.LNURLResponse{
Status: "Error",
Reason: "Couldn't create invoice."},
}
return response, err
}
// nip57 - we need to store the newly created invoice in the zap receipt
if zapEvent.Sig != "" {
pk := nostrPrivkeyHex
pub, _ := nostr.GetPublicKey(pk)
nip57Receipt = nostr.Event{
PubKey: pub,
CreatedAt: time.Now(),
Kind: 9735,
Tags: nostr.Tags{
*zapEvent.Tags.GetFirst([]string{"p"}),
[]string{"bolt11", invoice},
[]string{"description", zapEventSerializedStr},
},
}
if zapEvent.Tags.GetFirst([]string{"e"}) != nil {
nip57Receipt.Tags = nip57Receipt.Tags.AppendUnique(*zapEvent.Tags.GetFirst([]string{"e"}))
}
nip57Receipt.Sign(pk)
}
return LNURLPayValuesCustom{
LNURLResponse: lnurl.LNURLResponse{Status: "OK"},
PR: invoice,
Routes: make([]struct{}, 0),
SuccessAction: &lnurl.SuccessAction{Message: "Payment Received!", Tag: "message"},
nip57Receipt: nip57Receipt,
nip57ReceiptRelays: nip57ReceiptRelays,
}, nil
}