From 6e4af5610fe7b4c47638a6f2d0e7d664165dc8fb Mon Sep 17 00:00:00 2001 From: Michael Bumann Date: Sat, 28 Aug 2021 22:11:24 +0200 Subject: [PATCH] Add Lightning Address support This handles the LNURL-pay flow to enable Lightning Address support. It accepts any lightning address name on your domain. e.g. yourname@yourdomain.com --- README.md | 19 ++++++++++++++---- lnme.go | 45 +++++++++++++++++++++++++++++++++++++++++++ lnurl/types.go | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 112 insertions(+), 4 deletions(-) create mode 100644 lnurl/types.go diff --git a/README.md b/README.md index fdc2b91..870a682 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,10 @@ LnMe is a personal Bitcoin Lightning payment website and payment widget. ![demo](./lnme-demo.gif) -It is a small service written in Go that connects to a [lnd node](https://github.com/lightningnetwork/lnd/blob/master/docs/INSTALL.md) and exposes a simple HTTP JSON API to create and monitor invoices. +It is a small service written in Go that connects to a [lnd node](https://github.com/lightningnetwork/lnd/blob/master/docs/INSTALL.md) and exposes a simple HTTP JSON API to create and monitor invoices. It comes with a configurable personal payment website and offers a JavaScript widget to integrate in existing websites. -If [webln](https://github.com/wbobeirne/webln) is available the widget automatically use webln to request the payment; +If [webln](https://github.com/wbobeirne/webln) is available the widget automatically use webln to request the payment; otherwise an overlay will be shown with the payment request and a QR code. ## Motivation @@ -19,7 +19,7 @@ BTCPay Server is too big and hard to run for that and I do not need most of its ## Installation -LnMe connects to your [LND node](https://github.com/lightningnetwork/lnd/blob/master/docs/INSTALL.md), so a running LND node is required. +LnMe connects to your [LND node](https://github.com/lightningnetwork/lnd/blob/master/docs/INSTALL.md), so a running LND node is required. LnMe can easily run next to LND on the same system. 1. Download the latest [release](https://github.com/bumi/lnme/releases) @@ -55,6 +55,7 @@ Instead of the path to the macaroon and cert files you can also provide the hex * `static-path`: Path to a folder that you want to serve with LnMe (e.g. /home/bitcoin/lnme/website). Use this if you want to customize your ⚡website. default: disabled * `disable-website`: Disable the default LnMe website. Disable the website if you only want to embed the LnMe widget on your existing website. * `disable-cors`: Disable CORS headers. (default: false) +* `disable-ln-address`: Disable [Lightning Address](https://lightningaddress.com/) handling. * `port`: Port to listen on. (default: 1323) * `request-limit`: Limit the allowed requests per second. (default: 5) @@ -89,7 +90,7 @@ All environment variables must be prefixed by `LNME_` use `_` instead of `-` It is the easiest to run LnMe on the same node as LND. But you can run it anywhere as long as your LND node is accessible. -#### Heroku +#### Heroku One click deployment with Heroku: [![Deploy on Heroku](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/bumi/lnme) @@ -109,6 +110,16 @@ lnme.michaelbumann.com { `$ caddy --config /etc/caddy/Caddyfile` +### Lightning Address + +The Lightning Address is an Internet Identifier that allows anyone to send you Bitcoin over the Lightning Network. +Lightning Address builds on [LNURL-pay](https://github.com/fiatjaf/lnurl-rfc/blob/luds/06.md) LnMe handles the necessary requests for you. + +For more information check out the website: [lightningaddress.com](https://lightningaddress.com/) + +Your Lightning Address: `{anything}@{your domain}` + + ### Customize your ⚡ website LnMe comes with a default website but you can easily configure and build your own using the the LnMe JavaScript widget or JSON API. diff --git a/lnme.go b/lnme.go index c0d0264..74b5539 100644 --- a/lnme.go +++ b/lnme.go @@ -1,14 +1,19 @@ package main import ( + "crypto/sha256" + "encoding/hex" "flag" + "fmt" "log" "net/http" "os" + "strconv" "strings" rice "github.com/GeertJohan/go.rice" "github.com/bumi/lnme/ln" + "github.com/bumi/lnme/lnurl" "github.com/didip/tollbooth/v6" "github.com/didip/tollbooth/v6/limiter" "github.com/knadh/koanf" @@ -137,6 +142,45 @@ func main() { return c.JSON(http.StatusOK, invoice) }) + if !cfg.Bool("disable-ln-address") { + e.GET("/.well-known/lnurlp/:name", func(c echo.Context) error { + name := c.Param("name") + lightningAddress := name + "@" + c.Request().Host + lnurlMetadata := "[\"text/identifier\", \"" + lightningAddress + "\"], [\"text/plain\", \"Sats for " + lightningAddress + "\"]" + + if amount := c.QueryParam("amount"); amount == "" { + lnurlPayResponse1 := lnurl.LNURLPayResponse1{ + LNURLResponse: lnurl.LNURLResponse{Status: "OK"}, + Callback: fmt.Sprintf("%s://%s%s", c.Scheme(), c.Request().Host, c.Request().URL.Path), + MinSendable: 1000, + MaxSendable: 100000000, + EncodedMetadata: lnurlMetadata, + CommentAllowed: 0, + Tag: "payRequest", + } + return c.JSON(http.StatusOK, lnurlPayResponse1) + } else { + stdOutLogger.Printf("New LightningAddress request amount: %s", amount) + msats, err := strconv.ParseInt(amount, 10, 64) + if err != nil || msats < 1000 { + return c.JSON(http.StatusOK, lnurl.LNURLErrorResponse{Status: "ERROR", Reason: "Invalid Amount"}) + } + sats := msats / 1000 // we need sats + metadataHash := sha256.Sum256([]byte(lnurlMetadata)) + memo := hex.EncodeToString(metadataHash[:]) + invoice, err := lnClient.AddInvoice(sats, memo) + lnurlPayResponse2 := lnurl.LNURLPayResponse2{ + LNURLResponse: lnurl.LNURLResponse{Status: "OK"}, + PR: invoice.PaymentRequest, + Routes: make([][]lnurl.RouteInfo, 0), + Disposable: false, + SuccessAction: &lnurl.SuccessAction{Tag: "message", Message: "Thanks, payment received!"}, + } + return c.JSON(http.StatusOK, lnurlPayResponse2) + } + }) + } + // Debug test endpoint e.GET("/ping", func(c echo.Context) error { return c.JSON(http.StatusOK, "pong") @@ -157,6 +201,7 @@ func LoadConfig() *koanf.Koanf { f.String("lnd-macaroon-path", "~/.lnd/data/chain/bitcoin/mainnet/invoice.macaroon", "Path to the LND macaroon file.") f.String("lnd-cert-path", "~/.lnd/tls.cert", "Path to the LND tls.cert file.") f.Bool("disable-website", false, "Disable default embedded website.") + f.Bool("disable-ln-address", false, "Disable Lightning Address handling") f.Bool("disable-cors", false, "Disable CORS headers.") f.Float64("request-limit", 5, "Request limit per second.") f.String("static-path", "", "Path to a static assets directory.") diff --git a/lnurl/types.go b/lnurl/types.go new file mode 100644 index 0000000..ba60c09 --- /dev/null +++ b/lnurl/types.go @@ -0,0 +1,52 @@ +// THANKS: https://github.com/fiatjaf/go-lnurl/blob/d50a8e916232580895822178fe36e0f5cf400554/base.go +// only using the LNURL types here +package lnurl + +import "net/url" + +type LNURLResponse struct { + Status string `json:"status,omitempty"` + Reason string `json:"reason,omitempty"` +} + +type LNURLPayResponse1 struct { + LNURLResponse + Callback string `json:"callback"` + CallbackURL *url.URL `json:"-"` + Tag string `json:"tag"` + MaxSendable int64 `json:"maxSendable"` + MinSendable int64 `json:"minSendable"` + EncodedMetadata string `json:"metadata"` + Metadata Metadata `json:"-"` + CommentAllowed int64 `json:"commentAllowed"` +} + +type LNURLPayResponse2 struct { + LNURLResponse + SuccessAction *SuccessAction `json:"successAction"` + Routes [][]RouteInfo `json:"routes"` + PR string `json:"pr"` + Disposable bool `json:"disposable,omitempty"` +} + +type RouteInfo struct { + NodeId string `json:"nodeId"` + ChannelUpdate string `json:"channelUpdate"` +} + +type SuccessAction struct { + Tag string `json:"tag"` + Description string `json:"description,omitempty"` + URL string `json:"url,omitempty"` + Message string `json:"message,omitempty"` + Ciphertext string `json:"ciphertext,omitempty"` + IV string `json:"iv,omitempty"` +} + +type LNURLErrorResponse struct { + Status string `json:"status,omitempty"` + Reason string `json:"reason,omitempty"` + URL *url.URL `json:"-"` +} + +type Metadata [][]string