forked from nbd-wtf/satdress
-
Notifications
You must be signed in to change notification settings - Fork 1
/
lnurl.go
281 lines (251 loc) · 9.62 KB
/
lnurl.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
package main
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"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 uint64 = 1000
var maxSendable uint64 = 1000000000
var CommentAllowed int = 2000
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 response interface{}
username := mux.Vars(r)["user"]
domain := s.Domain
params := getParams(username)
if params == nil {
log.Debug().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 == "" {
// check if the receiver accepts comments
var commentLength int64 = 0
// TODO: support webhook comments
// 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 = DecodeBech32(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: makeMetadata(params),
CommentAllowed: commentLength,
Tag: "payRequest",
AllowsNostr: allowNostr,
NostrPubKey: nostrPubkey,
})
} else {
msat, err := strconv.ParseUint(amount, 10, 64)
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 ", "")
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 *UserParams, username string, amount_msat uint64, 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, 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 = "@" + EncodeBech32Public(zapEvent.PubKey)
if zapEvent.Tags.GetFirst([]string{"e"}) != nil {
note = "@" + EncodeBech32Note(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
}