mirror of
https://github.com/believethehype/nostdress.git
synced 2026-06-12 04:13:30 -07:00
310 lines
10 KiB
Go
310 lines
10 KiB
Go
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"`
|
|
Comment string `json:"comment"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
Paid bool `json:"paid"`
|
|
PaidAt time.Time `json:"paid_at"`
|
|
From string `json:"from"`
|
|
ParsedInvoice decodepay.Bolt11 `json:"-"`
|
|
PayerDataJSON string `json:"-"`
|
|
Nip57Receipt nostr.Event `json:"nip57Receipt"`
|
|
Nip57ReceiptRelays []string `json:"nip57ReceiptRelays"`
|
|
AwaitInvoicePaid bool `json:"awaitInvoicePaid"`
|
|
Sender string `json:"sender"`
|
|
Note string `json:"note"`
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
//if account is a forward account we simply redirect to the other address.
|
|
//everything is handled from there. In this case, we only provide Address/NIP05
|
|
if params.Kind == "forward" {
|
|
http.Redirect(w, r, params.Host, http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
log.Debug().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
|
|
}
|
|
|
|
//serveLNURLpFirst
|
|
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
|
|
}
|
|
|
|
var comment = ""
|
|
var payerData lnurl.PayerDataValues
|
|
// 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("Couldn't parse nostr event: ", err.Error())
|
|
} else {
|
|
valid, err := zapEvent.CheckSignature()
|
|
if !valid || err != nil {
|
|
log.Error().Err(err).Str("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("Nostr NIP-57 zap event validation error ", err.Error())
|
|
return
|
|
}
|
|
}
|
|
if len(zapEvent.Content) > 0 {
|
|
comment = zapEvent.Content
|
|
log.Debug().Str("NIP57 Comment received", comment).Msg("Comment")
|
|
}
|
|
|
|
}
|
|
//We can't handle comments and payerdata in NIP57 at the same time...
|
|
|
|
// If a comment is send with the Invoice, always use it (?)
|
|
regularcomment := r.FormValue("comment")
|
|
if len(regularcomment) > CommentAllowed {
|
|
log.Error().Err(err).Str("Comment is too long", err.Error())
|
|
return
|
|
}
|
|
if len(regularcomment) > 0 {
|
|
comment = regularcomment
|
|
log.Debug().Str("Comment received", comment).Msg("Comment")
|
|
}
|
|
// payer data, not used currently
|
|
payerdata := r.FormValue("payerdata")
|
|
|
|
if len(payerdata) > 0 {
|
|
err = json.Unmarshal([]byte(payerdata), &payerData)
|
|
if err != nil {
|
|
log.Error().Err(err).Str("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)
|
|
if err != 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,
|
|
})
|
|
|
|
//if we provided a nsec and the response contained zap information, we wait for the invoice to be paid
|
|
//in order to submit the zap on nostr
|
|
//also check for invoice paid for regular ln payments for nostr notificaitons
|
|
if payvaluescustom.AwaitInvoicePaid {
|
|
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 {
|
|
return LNURLPayValuesCustom{
|
|
LNURLResponse: lnurl.LNURLResponse{
|
|
Status: "Error",
|
|
Reason: "Couldn't serialize zap event."},
|
|
}, err
|
|
}
|
|
// we extract the relays from the zap request
|
|
nip57ReceiptRelays = ExtractNostrRelays(zapEvent)
|
|
|
|
} else {
|
|
//If we have a regular call, we ignore zapEvent in makeinvoice later.
|
|
zapEventSerializedStr = ""
|
|
log.Debug().Str("Regular Invoice", "Not an NIP57 event").Msg("Note")
|
|
}
|
|
|
|
var response LNURLPayValuesCustom
|
|
invoice, err := makeInvoice(params, amount_msat, nil, zapEventSerializedStr, comment)
|
|
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
|
|
}
|
|
|
|
//Check invoice paid only if we actually have a NIP57 event
|
|
var awaitPaid = true
|
|
var sender = ""
|
|
var note = ""
|
|
// nip57 - we need to store the newly created invoice in the zap receipt
|
|
if zapEvent.Sig != "" {
|
|
// TODO: Handle the err
|
|
nip57Receipt, err = CreateNostrReceipt(zapEvent, invoice)
|
|
sender = "@" + EncodeBench32Public(zapEvent.PubKey)
|
|
if zapEvent.Tags.GetFirst([]string{"e"}) != nil {
|
|
note = "@" + EncodeBench32Note(zapEvent.Tags.GetFirst([]string{"e"}).Value())
|
|
}
|
|
if zapEvent.Tags.GetFirst([]string{"anon"}) != nil {
|
|
if zapEvent.Tags.GetFirst([]string{"anon"}).Value() == "" {
|
|
sender = "anonymous Zapper 🤙"
|
|
}
|
|
}
|
|
log.Debug().Str("Zap from", sender).Msg("Nostr")
|
|
}
|
|
|
|
decoded_invoice, _ := decodepay.Decodepay(invoice)
|
|
return LNURLPayValuesCustom{
|
|
LNURLResponse: lnurl.LNURLResponse{Status: "OK"},
|
|
PR: invoice,
|
|
Routes: make([]struct{}, 0),
|
|
SuccessAction: &lnurl.SuccessAction{Message: "Payment Received!", Tag: "message"},
|
|
Comment: comment,
|
|
Paid: false,
|
|
CreatedAt: time.Now(),
|
|
ParsedInvoice: decoded_invoice,
|
|
Nip57Receipt: nip57Receipt,
|
|
Nip57ReceiptRelays: nip57ReceiptRelays,
|
|
AwaitInvoicePaid: awaitPaid,
|
|
Sender: sender,
|
|
Note: note,
|
|
}, nil
|
|
|
|
}
|