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
This commit is contained in:
bumi 2021-08-28 22:11:24 +02:00
parent 5ca9e72ab0
commit 6e4af5610f
3 changed files with 112 additions and 4 deletions

View File

@ -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.

45
lnme.go
View File

@ -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.")

52
lnurl/types.go Normal file
View File

@ -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