mirror of https://github.com/bumi/lntip
Comments and documentation
This commit is contained in:
parent
c0171c0e81
commit
05fa7500b0
88
README.md
88
README.md
|
@ -1,88 +1,108 @@
|
||||||
# LnTip - your friendly lightning tipping widget
|
# LnMe - notBTCPayServer but your friendly ⚡ payment page
|
||||||
|
|
||||||
LnTip provides a Bitcoin lightning tipping widget that can easily be integrated into any website.
|
LnMe is your personal Bitcoin Lightning payment website and payment widget.
|
||||||
|
|
||||||
It consistes of a small service written in Go that connects to a lnd node and exposes
|
It is a small service written in Go that connects to a lnd node and exposes a simple HTTP JSON API to create and monitor invoices.
|
||||||
a simple HTTP JSON API to create and monitor invoices. This is a HTTP/REST proxy to the LND add and receive invoices API.
|
It comes with a configurable personal ⚡ website and offers a JavaScript widget to integrate in existing websites.
|
||||||
|
|
||||||
That API is consumed from a tiny JavaScript widget that can be integrated into any website.
|
|
||||||
|
|
||||||
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.
|
otherwise an overlay will be shown with the payment request and a QR code.
|
||||||
|
|
||||||
|
|
||||||
## Motivation
|
## Motivation
|
||||||
|
|
||||||
I wanted a simple tipping button for my website that uses my own lightning node and does not rely on external services (does not need to trusts external services to handle the payments and hold the coins).
|
I wanted a simple way for people to send Lightning payments to me using my own lightning node.
|
||||||
|
|
||||||
|
BTCPay Server is too big and hard to run for that as I do not need most features.
|
||||||
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
To use LnTip a running [LND node](https://github.com/lightningnetwork/lnd/blob/master/docs/INSTALL.md)
|
LnMe connects to your [LND node](https://github.com/lightningnetwork/lnd/blob/master/docs/INSTALL.md), so a running LND node is required.
|
||||||
is required.
|
LnMe can easily run next to LND.
|
||||||
|
|
||||||
1. download the latest [release](https://github.com/bumi/lntip/releases)
|
1. Download the latest [release](https://github.com/bumi/lntip/releases)
|
||||||
2. run `invoices_proxy` (to run it as systemd service have a look at the [systemd service example config](https://github.com/bumi/lntip/blob/master/examples/invoices-proxy.service))
|
2. Run `lnme` (to run it as systemd service have a look at the [systemd service example config](https://github.com/bumi/lntip/blob/master/examples/invoices-proxy.service))
|
||||||
3. integrate the widget on website
|
3. Done.
|
||||||
|
|
||||||
### Configuration
|
### Configuration
|
||||||
|
|
||||||
To connect to the lnd node the cert, macaroon and address of the lnd node has to be configured:
|
#### LND configuration
|
||||||
|
|
||||||
|
To connect to the lnd node the cert, macaroon and address of the lnd node has to be configured. LnMe uses the LND defaults.
|
||||||
|
|
||||||
* address: Host and port of the lnd gRPC service. default: localhost:10009
|
* address: Host and port of the lnd gRPC service. default: localhost:10009
|
||||||
* cert: Path to the lnd cert file. default: ~/.lnd/tls.cert
|
* cert: Path to the lnd cert file. default: ~/.lnd/tls.cert
|
||||||
* macaroon: Path to the macaroon file. default: ~/.lnd/data/chain/bitcoin/mainnet/invoice.macaroon
|
* macaroon: Path to the macaroon file. default: ~/.lnd/data/chain/bitcoin/mainnet/invoice.macaroon
|
||||||
* bind: Host and port to listen on. default: :1323 (localhost:1323)
|
|
||||||
* static-path: The proxy can serve files from a static folder (e.g. the JS/CSS files). Use this option to configure the path to a filder. (e.g. /home/bitcoin/lntip/assets) default: disabled
|
#### Other configuration
|
||||||
|
|
||||||
|
* 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-cors: Disable CORS headers. (default: false)
|
||||||
* request-limit: Limit the allowed requests per second. (default: 10)
|
* bind: Host and port to listen on. (default: :1323)
|
||||||
|
* request-limit: Limit the allowed requests per second. (default: 5)
|
||||||
|
|
||||||
Examples:
|
#### Examples:
|
||||||
|
|
||||||
$ ./invoices_proxy --help
|
$ ./lnme --help
|
||||||
$ ./invoices_proxy --address=lndhost.com:10009 --bind=localhost:4711
|
$ ./lnme --address=lndhost.com:10009 --bind=localhost:4711
|
||||||
|
$ ./lnme --disable-website
|
||||||
|
|
||||||
|
|
||||||
|
### Customize your ⚡ website
|
||||||
|
|
||||||
|
LnMe comes with a default website but you can easily configure and build your own website and use the LnMe widget.
|
||||||
|
|
||||||
|
Take a look at the [embedded default website](https://github.com/bumi/lntip/blob/master/files/root/index.html) for an example and use the `--static-path` option to configure LnMe to serve your static file.
|
||||||
|
|
||||||
|
1. Create a new folder (e.g. /home/satoshi/my-ln-page)
|
||||||
|
2. Create your index.html
|
||||||
|
3. Run lnme: `lnme --static-path=/home/satoshi/my-ln-page
|
||||||
|
|
||||||
|
|
||||||
### JavaScript Widget integration
|
### JavaScript Widget integration
|
||||||
|
|
||||||
Load the JavaScript file in your HTML page and configure the `lntip-host` attribute
|
You can integrate the LnMe widget in your existing website.
|
||||||
to the host and port on which your lntip instance is running:
|
|
||||||
|
#### 1. Add the LnMe JavaScript files
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<script lntip-host="https://your-lntip-host.com:1323" src="https://cdn.jsdelivr.net/gh/bumi/lntip/assets/lntip.js" id="lntip-script"></script>
|
<script data-lnme-base-url="https://your-lnme-host.com:1323" src="https://your-lnme-host.com/lnme/lnme.js"></script>
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Usage
|
#### 2. Usage
|
||||||
|
|
||||||
To request a lightning payment simply call `request()` on a `new LnTip({value: value, memo: memo})`:
|
To request a lightning payment simply call `request()` on a `new LnMe({value: value, memo: memo})`:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
new LnTip({ value: 1000, memo: 'high5' }).request()
|
var lnme = new LnMe({ value: 1000, memo: 'high5' });
|
||||||
|
lnme.request();
|
||||||
```
|
```
|
||||||
|
|
||||||
Use it from a plain HTML link:
|
Use it from a plain HTML link:
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<a href="#" onclick="javascript:new LnTip({ value: 1000, memo: 'high5' }).request();return false;">Tip me</a>
|
<a href="#" onclick="javascript:new LnMe({ value: 1000, memo: 'high5' }).request();return false;">Tip me</a>
|
||||||
```
|
```
|
||||||
|
|
||||||
##### More advanced JS API:
|
##### More advanced JS API:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
let tip = new LnTip({ value: 1000, memo: 'high5' });
|
let lnme = new LnMe({ value: 1000, memo: 'high5' });
|
||||||
|
|
||||||
// get a new invoice and watch for a payment
|
// get a new invoice and watch for a payment
|
||||||
// promise resolves if the invoice is settled
|
// promise resolves if the invoice is settled
|
||||||
tip.requestPayment().then((invoice) => {
|
lnme.requestPayment().then(invoice => {
|
||||||
alert('YAY, thanks!');
|
alert('YAY, thanks!');
|
||||||
});
|
});
|
||||||
|
|
||||||
// create a new invoice
|
// create a new invoice
|
||||||
tip.addInvoice().then((invoice) => {
|
lnme.addInvoice().then(invoice => {
|
||||||
console.log(invoice.PaymentRequest)
|
console.log(invoice.PaymentRequest)
|
||||||
});
|
});
|
||||||
|
|
||||||
// periodically watch if an invoice is settled
|
// periodically watch if an invoice is settled
|
||||||
tip.watchPayment().then((invoice) => {
|
lnme.watchPayment().then(invoice => {
|
||||||
alert('YAY, thanks!');
|
alert('YAY, thanks!');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -90,14 +110,14 @@ tip.watchPayment().then((invoice) => {
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
Use `go run` to ron the service locally:
|
Use `go run` to ron the service locally:
|
||||||
|
|
||||||
$ go run invoices_proxy.go --address=127.0.0.1:10009 --cert=/home/bitcoin/lightning/tls.cert --macaroon=/home/bitcoin/lightning/invoice.macaroon
|
$ go run lnme.go --address=127.0.0.1:10009 --cert=/home/bitcoin/lightning/tls.cert --macaroon=/home/bitcoin/lightning/invoice.macaroon
|
||||||
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Bug reports and pull requests are welcome on GitHub at https://github.com/bumi/lntip
|
Bug reports and pull requests are welcome on GitHub at https://github.com/bumi/lnme
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|
24
ln/lnd.go
24
ln/lnd.go
|
@ -25,6 +25,13 @@ type Invoice struct {
|
||||||
Settled bool `json:"settled"`
|
Settled bool `json:"settled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LNDoptions are the options for the connection to the lnd node.
|
||||||
|
type LNDoptions struct {
|
||||||
|
Address string
|
||||||
|
CertFile string
|
||||||
|
MacaroonFile string
|
||||||
|
}
|
||||||
|
|
||||||
type LNDclient struct {
|
type LNDclient struct {
|
||||||
lndClient lnrpc.LightningClient
|
lndClient lnrpc.LightningClient
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
|
@ -50,6 +57,7 @@ func (c LNDclient) AddInvoice(value int64, memo string) (Invoice, error) {
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewAddress gets the next BTC onchain address.
|
||||||
func (c LNDclient) NewAddress() (string, error) {
|
func (c LNDclient) NewAddress() (string, error) {
|
||||||
stdOutLogger.Printf("Getting a new BTC address")
|
stdOutLogger.Printf("Getting a new BTC address")
|
||||||
request := lnrpc.NewAddressRequest{
|
request := lnrpc.NewAddressRequest{
|
||||||
|
@ -130,19 +138,3 @@ func NewLNDclient(lndOptions LNDoptions) (LNDclient, error) {
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// LNDoptions are the options for the connection to the lnd node.
|
|
||||||
type LNDoptions struct {
|
|
||||||
// Address of your LND node, including the port.
|
|
||||||
// Optional ("localhost:10009" by default).
|
|
||||||
Address string
|
|
||||||
// Path to the "tls.cert" file that your LND node uses.
|
|
||||||
// Optional ("tls.cert" by default).
|
|
||||||
CertFile string
|
|
||||||
// Path to the macaroon file that your LND node uses.
|
|
||||||
// "invoice.macaroon" if you only use the AddInvoice() and GetInvoice() methods
|
|
||||||
// (required by the middleware in the package "wall").
|
|
||||||
// "admin.macaroon" if you use the Pay() method (required by the client in the package "pay").
|
|
||||||
// Optional ("invoice.macaroon" by default).
|
|
||||||
MacaroonFile string
|
|
||||||
}
|
|
||||||
|
|
51
lnme.go
51
lnme.go
|
@ -13,7 +13,8 @@ import (
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
// move to file
|
// Middleware for request limited to prevent too many requests
|
||||||
|
// TODO: move to file
|
||||||
func LimitMiddleware(lmt *limiter.Limiter) echo.MiddlewareFunc {
|
func LimitMiddleware(lmt *limiter.Limiter) echo.MiddlewareFunc {
|
||||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
return echo.HandlerFunc(func(c echo.Context) error {
|
return echo.HandlerFunc(func(c echo.Context) error {
|
||||||
|
@ -34,45 +35,55 @@ type Invoice struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
address := flag.String("address", "localhost:10009", "The host and port of the ln gRPC server")
|
address := flag.String("address", "localhost:10009", "The host and port of the lnd gRPC server.")
|
||||||
certFile := flag.String("cert", "~/.lnd/tls.cert", "Path to the lnd tls.cert file")
|
certFile := flag.String("cert", "~/.lnd/tls.cert", "Path to the lnd tls.cert file.")
|
||||||
macaroonFile := flag.String("macaroon", "~/.lnd/data/chain/bitcoin/mainnet/invoice.macaroon", "Path to the lnd macaroon file")
|
macaroonFile := flag.String("macaroon", "~/.lnd/data/chain/bitcoin/mainnet/invoice.macaroon", "Path to the lnd macaroon file.")
|
||||||
bind := flag.String("bind", ":1323", "Host and port to bind on")
|
bind := flag.String("bind", ":1323", "Host and port to bind on.")
|
||||||
staticPath := flag.String("static-path", "", "Path to a static assets directory. Blank to not serve any static files")
|
staticPath := flag.String("static-path", "", "Path to a static assets directory. Leave blank to not serve any static files.")
|
||||||
disableHTML := flag.Bool("disable-html", false, "Disable HTML page")
|
disableWebsite := flag.Bool("disable-website", false, "Disable default embedded website.")
|
||||||
disableCors := flag.Bool("disable-cors", false, "Disable CORS headers")
|
disableCors := flag.Bool("disable-cors", false, "Disable CORS headers.")
|
||||||
requestLimit := flag.Float64("request-limit", 5, "Request limit per second")
|
requestLimit := flag.Float64("request-limit", 5, "Request limit per second.")
|
||||||
|
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
e := echo.New()
|
e := echo.New()
|
||||||
|
|
||||||
|
// Serve static files if configured
|
||||||
if *staticPath != "" {
|
if *staticPath != "" {
|
||||||
e.Static("/", *staticPath)
|
e.Static("/", *staticPath)
|
||||||
} else if !*disableHTML {
|
// Serve default page
|
||||||
|
} else if !*disableWebsite {
|
||||||
rootBox := rice.MustFindBox("files/root")
|
rootBox := rice.MustFindBox("files/root")
|
||||||
indexPage, err := rootBox.String("index.html")
|
indexPage, err := rootBox.String("index.html")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
stdOutLogger.Print("Running page")
|
stdOutLogger.Print("Running embedded page")
|
||||||
e.GET("/", func(c echo.Context) error {
|
e.GET("/", func(c echo.Context) error {
|
||||||
return c.HTML(200, indexPage)
|
return c.HTML(200, indexPage)
|
||||||
})
|
})
|
||||||
}
|
} else {
|
||||||
|
stdOutLogger.Printf("Failed to run embedded website: %s", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// Embed static files and serve those on /lnme (e.g. /lnme/lnme.js)
|
||||||
|
assetHandler := http.FileServer(rice.MustFindBox("files/assets").HTTPBox())
|
||||||
|
e.GET("/lnme/*", echo.WrapHandler(http.StripPrefix("/lnme/", assetHandler)))
|
||||||
|
|
||||||
|
|
||||||
|
// CORS settings
|
||||||
if !*disableCors {
|
if !*disableCors {
|
||||||
e.Use(middleware.CORS())
|
e.Use(middleware.CORS())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Recover middleware recovers from panics anywhere in the request chain
|
||||||
e.Use(middleware.Recover())
|
e.Use(middleware.Recover())
|
||||||
|
|
||||||
|
// Request limit per second. DoS protection
|
||||||
if *requestLimit > 0 {
|
if *requestLimit > 0 {
|
||||||
limiter := tollbooth.NewLimiter(*requestLimit, nil)
|
limiter := tollbooth.NewLimiter(*requestLimit, nil)
|
||||||
e.Use(LimitMiddleware(limiter))
|
e.Use(LimitMiddleware(limiter))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Embed static files and serve those on /lnme (e.g. /lnme/lnme.js)
|
// Setup lightning client
|
||||||
assetHandler := http.FileServer(rice.MustFindBox("files/assets").HTTPBox())
|
|
||||||
e.GET("/lnme/*", echo.WrapHandler(http.StripPrefix("/lnme/", assetHandler)))
|
|
||||||
|
|
||||||
stdOutLogger.Printf("Connection to %s using macaroon %s and cert %s", *address, *macaroonFile, *certFile)
|
stdOutLogger.Printf("Connection to %s using macaroon %s and cert %s", *address, *macaroonFile, *certFile)
|
||||||
lndOptions := ln.LNDoptions{
|
lndOptions := ln.LNDoptions{
|
||||||
Address: *address,
|
Address: *address,
|
||||||
|
@ -85,7 +96,9 @@ func main() {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// endpoint URLs compatible to the LND REST API
|
// Endpoint URLs compatible to the LND REST API v1
|
||||||
|
//
|
||||||
|
// Create new invoice
|
||||||
e.POST("/v1/invoices", func(c echo.Context) error {
|
e.POST("/v1/invoices", func(c echo.Context) error {
|
||||||
i := new(Invoice)
|
i := new(Invoice)
|
||||||
if err := c.Bind(i); err != nil {
|
if err := c.Bind(i); err != nil {
|
||||||
|
@ -102,6 +115,7 @@ func main() {
|
||||||
return c.JSON(http.StatusOK, invoice)
|
return c.JSON(http.StatusOK, invoice)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Get next BTC onchain address
|
||||||
e.POST("/v1/newaddress", func(c echo.Context) error {
|
e.POST("/v1/newaddress", func(c echo.Context) error {
|
||||||
address, err := lnClient.NewAddress()
|
address, err := lnClient.NewAddress()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -111,6 +125,7 @@ func main() {
|
||||||
return c.JSON(http.StatusOK, address)
|
return c.JSON(http.StatusOK, address)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Check invoice status
|
||||||
e.GET("/v1/invoice/:invoiceId", func(c echo.Context) error {
|
e.GET("/v1/invoice/:invoiceId", func(c echo.Context) error {
|
||||||
invoiceId := c.Param("invoiceId")
|
invoiceId := c.Param("invoiceId")
|
||||||
invoice, err := lnClient.GetInvoice(invoiceId)
|
invoice, err := lnClient.GetInvoice(invoiceId)
|
||||||
|
@ -123,7 +138,7 @@ func main() {
|
||||||
return c.JSON(http.StatusOK, invoice)
|
return c.JSON(http.StatusOK, invoice)
|
||||||
})
|
})
|
||||||
|
|
||||||
// debug test endpoint
|
// Debug test endpoint
|
||||||
e.GET("/ping", func(c echo.Context) error {
|
e.GET("/ping", func(c echo.Context) error {
|
||||||
return c.JSON(http.StatusOK, "pong")
|
return c.JSON(http.StatusOK, "pong")
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in New Issue