Skip to content

Commit

Permalink
Lnurl (#30)
Browse files Browse the repository at this point in the history
* dunno

* fix user lookup

* lnurl port

* fix host

* add config example

* amount error msg

* print stack trace

* lnurl_public_server

* remove public server !

* smaller

* smaller

* webhooks

* add tag to response

* add EncodedMetadata and Status

* add success action and empty routes

* update invoice memo

* description hash

* dunno

* fix user lookup

* lnurl port

* fix host

* amount error msg

* add config example

* print stack trace

* lnurl_public_server

* remove public server !

* smaller

* smaller

* webhooks

* add tag to response

* add EncodedMetadata and Status

* add success action and empty routes

* update invoice memo

* description hash

Co-authored-by: gohumble <[email protected]>
  • Loading branch information
LightningTipBot and gohumble authored Aug 30, 2021
1 parent f3b3e34 commit 77c3a50
Show file tree
Hide file tree
Showing 2 changed files with 86 additions and 49 deletions.
9 changes: 5 additions & 4 deletions internal/lnbits/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,11 @@ func (u *User) ResetState() {
}

type InvoiceParams struct {
Out bool `json:"out"`
Amount int64 `json:"amount"`
Memo string `json:"memo"` // the invoice description.
Webhook string `json:"webhook,omitempty"`
Out bool `json:"out"` // must be True if invoice is payed, False if invoice is received
Amount int64 `json:"amount"` // amount in MilliSatoshi
Memo string `json:"memo,omitempty"` // the invoice memo.
Webhook string `json:"webhook,omitempty"` // the webhook to fire back to when payment is received.
DescriptionHash string `json:"description_hash,omitempty"` // the invoice description hash.
}

type PaymentParams struct {
Expand Down
126 changes: 81 additions & 45 deletions internal/lnurl/server.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package lnurl

import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"net"
Expand Down Expand Up @@ -61,19 +63,84 @@ func NewServer(lnurlserver string, webhookserver string, bot *tb.Bot, client *ln
func (w *Server) newRouter() *mux.Router {
router := mux.NewRouter()
router.HandleFunc(lnurlEndpoint+"/{username}", w.handleLnUrl).Methods(http.MethodGet)
router.HandleFunc("/@{username}", w.handleLnUrl).Methods(http.MethodGet)
//router.HandleFunc("/.well-know/lnurlp/{username}", w.handleLnUrl).Methods(http.MethodGet)
return router
}

func (w Server) handleLnUrl(writer http.ResponseWriter, request *http.Request) {
if request.URL.RawQuery == "" {
w.createInitialLNURLPayResponse(writer, request)
var err error
err = w.serveLNURLpFirst(writer, request)
if err != nil {
log.Errorln(err)
return
}
} else {
w.createLNURLPayResponse(writer, request)
w.serveLNURLpSecond(writer, request)
}
}

// descriptionHash is the SHA256 hash of the metadata
func (w Server) descriptionHash(metadata lnurl.Metadata, writer http.ResponseWriter) string {
jsonMeta, err := json.Marshal(metadata)
if err != nil {
writer.WriteHeader(400)
return ""
}
hash := sha256.Sum256([]byte(string(jsonMeta)))
hash_string := hex.EncodeToString(hash[:])
return hash_string
}

// metaData returns the metadata that is sent in the first response
// and is used again in the second response to verify the description hash
func (w Server) metaData(request *http.Request) lnurl.Metadata {
vars := mux.Vars(request)
return lnurl.Metadata{{"text/identifier", fmt.Sprintf("%[email protected]", vars["username"])}, {"text/plain", fmt.Sprintf("Pay to %s@%s", vars["username"], w.callbackUrl)}}
}

// serveLNURLpFirst serves the first part of the LNURLp protocol with the endpoint
// to call and the metadata that matches the description hash of the second response
func (w Server) serveLNURLpFirst(writer http.ResponseWriter, request *http.Request) error {
vars := mux.Vars(request)

callback := fmt.Sprintf("%s%s/%s", w.callbackUrl, lnurlEndpoint, vars["username"])
callbackURL, err := url.Parse(callback)
if err != nil {
log.Errorln("callback is not a valid URL")
writer.WriteHeader(400)
return err
}
metadata := w.metaData(request)
jsonMeta, err := json.Marshal(metadata)
if err != nil {
writer.WriteHeader(400)
return err
}

resp := lnurl.LNURLPayResponse1{
LNURLResponse: lnurl.LNURLResponse{Status: "OK"},
Tag: "payRequest",
Callback: fmt.Sprintf("https://%s", callback),
CallbackURL: callbackURL,
MinSendable: minSendable,
MaxSendable: MaxSendable,
CommentAllowed: 512,
EncodedMetadata: string(jsonMeta),
}
jsonResponse, err := json.Marshal(resp)
if err != nil {
writer.WriteHeader(400)
return err
}
writer.WriteHeader(200)
writer.Write(jsonResponse)
return nil
}

func (w Server) createLNURLPayResponse(writer http.ResponseWriter, request *http.Request) {
// serveLNURLpSecond serves the second LNURL response with the payment request with the correct description hash
func (w Server) serveLNURLpSecond(writer http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
user := &lnbits.User{}
tx := w.database.Where("telegram_username = ?", vars["username"]).First(user)
Expand All @@ -88,7 +155,7 @@ func (w Server) createLNURLPayResponse(writer http.ResponseWriter, request *http
stringAmount := request.FormValue("amount")
amount, err := strconv.Atoi(stringAmount)
if err != nil {
errmsg := fmt.Sprintf("[createLNURLPayResponse] Couldn't cast amount to int")
errmsg := fmt.Sprintf("[serveLNURLpSecond] Couldn't cast amount to int")
log.Warnln(errmsg)
return
}
Expand All @@ -97,22 +164,26 @@ func (w Server) createLNURLPayResponse(writer http.ResponseWriter, request *http

if amount < minSendable || amount > MaxSendable {
// amount is not ok
errmsg := fmt.Sprintf("[createLNURLPayResponse] Amount out of bounds")
errmsg := fmt.Sprintf("[serveLNURLpSecond] Amount out of bounds")
log.Warnln(errmsg)
resp = lnurl.LNURLPayResponse2{
LNURLResponse: lnurl.LNURLResponse{Status: "ERROR", Reason: fmt.Sprintf("Amount out of bounds (min: %d mSat, max: %d mSat).", minSendable, MaxSendable)},
}
} else {
// amount is ok

// the same description_hash needs to be built in the second request
metadata := w.metaData(request)
hash_string := w.descriptionHash(metadata, writer)
invoice, err := user.Wallet.Invoice(
lnbits.InvoiceParams{
Amount: int64(amount / 1000),
Out: false,
Memo: fmt.Sprintf("Pay to %s@%s", vars["username"], w.callbackUrl),
Webhook: w.WebhookServer},
Amount: int64(amount / 1000),
Out: false,
DescriptionHash: hash_string,
Webhook: w.WebhookServer},
*user.Wallet)
if err != nil {
errmsg := fmt.Sprintf("[createLNURLPayResponse] Couldn't create invoice: %s", err.Error())
errmsg := fmt.Sprintf("[serveLNURLpSecond] Couldn't create invoice: %s", err.Error())
log.Warnln(errmsg)
resp = lnurl.LNURLPayResponse2{
LNURLResponse: lnurl.LNURLResponse{Status: "ERROR", Reason: "Couldn't create invoice."},
Expand All @@ -134,38 +205,3 @@ func (w Server) createLNURLPayResponse(writer http.ResponseWriter, request *http
}
writer.Write(jsonResponse)
}

func (w Server) createInitialLNURLPayResponse(writer http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)

callback := fmt.Sprintf("%s%s/%s", w.callbackUrl, lnurlEndpoint, vars["username"])
callbackURL, err := url.Parse(callback)
if err != nil {
log.Errorln("callback is not a valid URL")
writer.WriteHeader(400)
return
}
metadata := lnurl.Metadata{{fmt.Sprintf("Pay to %s@%s", vars["username"], w.callbackUrl)}}
jsonMeta, err := json.Marshal(metadata)
if err != nil {
writer.WriteHeader(400)
return
}
resp := lnurl.LNURLPayResponse1{
LNURLResponse: lnurl.LNURLResponse{Status: "OK"},
Tag: "payRequest",
Callback: fmt.Sprintf("https://%s", callback),
CallbackURL: callbackURL,
MinSendable: minSendable,
MaxSendable: MaxSendable,
CommentAllowed: 512,
EncodedMetadata: string(jsonMeta),
}
jsonResponse, err := json.Marshal(resp)
if err != nil {
writer.WriteHeader(400)
return
}
writer.WriteHeader(200)
writer.Write(jsonResponse)
}

0 comments on commit 77c3a50

Please sign in to comment.