Refactor to mimic the lnd rest API a bit

This commit is contained in:
bumi 2019-02-19 20:55:07 +01:00
parent d3e7dc0668
commit 01fb7be447
5 changed files with 126 additions and 111 deletions

View File

@ -50,21 +50,21 @@ to the host and port on which your lntip instance is running:
#### Usage #### Usage
To request a lightning payment simply call `request()` on a `new LnTip({amount: amount, memo: memo})`: To request a lightning payment simply call `request()` on a `new LnTip({value: value, memo: memo})`:
```js ```js
new LnTip({ amount: 1000, memo: 'high5' }).request() new LnTip({ value: 1000, memo: 'high5' }).request()
``` ```
Use it from a plain HTML link: Use it from a plain HTML link:
```html ```html
<a href="#" onclick="javascript:new LnTip({ amount: 1000, memo: 'high5' }).request();return false;">Tip me</a> <a href="#" onclick="javascript:new LnTip({ value: 1000, memo: 'high5' }).request();return false;">Tip me</a>
``` ```
##### More advanced JS API: ##### More advanced JS API:
```js ```js
let tip = new LnTip({ amount: 1000, memo: 'high5' }); let tip = new LnTip({ 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
@ -73,7 +73,7 @@ tip.requestPayment().then((invoice) => {
}); });
// create a new invoice // create a new invoice
tip.getInvoice().then((invoice) => { tip.addInvoice().then((invoice) => {
console.log(invoice.PaymentRequest) console.log(invoice.PaymentRequest)
}); });
@ -84,6 +84,9 @@ tip.watchPayment().then((invoice) => {
``` ```
## Development
## Contributing ## Contributing

View File

@ -4,7 +4,7 @@
LnTip = function (options) { LnTip = function (options) {
var host = document.getElementById('lntip-script').getAttribute('lntip-host'); var host = document.getElementById('lntip-script').getAttribute('lntip-host');
this.host = options.host || host; this.host = options.host || host;
this.amount = options.amount; this.value = options.value;
this.memo = options.memo || ''; this.memo = options.memo || '';
this.loadStylesheet(); // load it early that styles are ready when the popup is opened this.loadStylesheet(); // load it early that styles are ready when the popup is opened
} }
@ -52,9 +52,9 @@ LnTip.prototype.watchPayment = function () {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.paymentWatcher = window.setInterval(() => { this.paymentWatcher = window.setInterval(() => {
this._fetch(`${this.host}/settled/${this.invoice.ImplDepID}`) this._fetch(`${this.host}/v1/invoice/${this.invoice.ImplDepID}`)
.then((settled) => { .then((invoice) => {
if (settled) { if (invoice.settled) {
this.invoice.settled = true; this.invoice.settled = true;
this.stopWatchingPayment(); this.stopWatchingPayment();
resolve(this.invoice); resolve(this.invoice);
@ -72,25 +72,25 @@ LnTip.prototype.stopWatchingPayment = function () {
LnTip.prototype.payWithWebln = function () { LnTip.prototype.payWithWebln = function () {
if (!webln.isEnabled) { if (!webln.isEnabled) {
webln.enable().then((weblnResponse) => { webln.enable().then((weblnResponse) => {
return webln.sendPayment({ paymentRequest: this.invoice.PaymentRequest }) return webln.sendPayment({ paymentRequest: this.invoice.payment_request })
}).catch((e) => { }).catch((e) => {
return this.showPaymentRequest(); return this.showPaymentRequest();
}) })
} else { } else {
return webln.sendPayment({ paymentRequest: this.invoice.PaymentRequest }) return webln.sendPayment({ paymentRequest: this.invoice.payment_request })
} }
} }
LnTip.prototype.showPaymentRequest = function () { LnTip.prototype.showPaymentRequest = function () {
var content = `<div class="lntip-payment-request"> var content = `<div class="lntip-payment-request">
<h1>${this.memo}</h1> <h1>${this.memo}</h1>
<h2>${this.amount} satoshi</h2> <h2>${this.value} satoshi</h2>
<div class="lntip-qr"> <div class="lntip-qr">
<img src="https://chart.googleapis.com/chart?cht=qr&chs=200x200&chl=${this.invoice.PaymentRequest}"> <img src="https://chart.googleapis.com/chart?cht=qr&chs=200x200&chl=${this.invoice.payment_request}">
</div> </div>
<div class="lntip-details"> <div class="lntip-details">
<a href="lightning:${this.invoice.PaymentRequest}" class="lntip-invoice"> <a href="lightning:${this.invoice.payment_request}" class="lntip-invoice">
${this.invoice.PaymentRequest} ${this.invoice.payment_request}
</a> </a>
<div class="lntip-copy" id="lntip-copy"> <div class="lntip-copy" id="lntip-copy">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-copy"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-copy"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>
@ -100,30 +100,30 @@ LnTip.prototype.showPaymentRequest = function () {
this.openPopup(content); this.openPopup(content);
document.getElementById('lntip-copy').onclick = () => { document.getElementById('lntip-copy').onclick = () => {
navigator.clipboard.writeText(this.invoice.PaymentRequest); navigator.clipboard.writeText(this.invoice.payment_request);
alert('Copied to clipboad'); alert('Copied to clipboad');
} }
return Promise.resolve(); // be compatible to payWithWebln() return Promise.resolve(); // be compatible to payWithWebln()
} }
LnTip.prototype.getInvoice = function () { LnTip.prototype.addInvoice = function () {
var args = { var args = {
method: 'POST', method: 'POST',
mode: 'cors', mode: 'cors',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ memo: this.memo, amount: this.amount }) body: JSON.stringify({ memo: this.memo, value: this.value })
}; };
return this._fetch( return this._fetch(
`${this.host}/invoice`, `${this.host}/v1/invoices`,
args args
).then((invoice) => { ).then((invoice) => {
this.invoice = invoice; this.invoice = invoice;
return invoice; return invoice;
}) });
} }
LnTip.prototype.requestPayment = function () { LnTip.prototype.requestPayment = function () {
return this.getInvoice().then((invoice) => { return this.addInvoice().then((invoice) => {
if (typeof webln !== 'undefined') { if (typeof webln !== 'undefined') {
return this.payWithWebln(); return this.payWithWebln();
} else { } else {
@ -142,6 +142,10 @@ LnTip.prototype.request = function () {
LnTip.prototype._fetch = function(url, args) { LnTip.prototype._fetch = function(url, args) {
return fetch(url, args).then((response) => { return fetch(url, args).then((response) => {
return response.json(); if (response.ok) {
return response.json();
} else {
throw new Error(response);
}
}) })
} }

View File

@ -5,8 +5,8 @@
<title></title> <title></title>
</head> </head>
<body> <body>
<script src="http://localhost:1323/static/lntip.js" lntip-host="http://localhost:1323"></script> <script lntip-host="http://localhost:1323" src="http://localhost:1323/static/lntip.js" id="lntip-script"></script>
<a href="#" onclick="javascript:new LnTip(1000, 'thanks');return false;">Tip me</a> <a href="#" onclick="javascript:new LnTip({ value: 1000, memo: 'thanks' }).request();return false;">Tip me</a>
</body> </body>
</html> </html>

View File

@ -13,7 +13,7 @@ import (
var stdOutLogger = log.New(os.Stdout, "", log.LstdFlags) var stdOutLogger = log.New(os.Stdout, "", log.LstdFlags)
type Invoice struct { type Invoice struct {
Amount int64 `json:"amount"` Value int64 `json:"value"`
Memo string `json:"memo"` Memo string `json:"memo"`
} }
@ -22,14 +22,21 @@ func main() {
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 disable serving static files")
disableCors := flag.Bool("disable-cors", false, "Disable CORS headers")
flag.Parse() flag.Parse()
e := echo.New() e := echo.New()
e.Static("/static", "assets") if (*staticPath != "") {
e.Use(middleware.CORS()) e.Static("/static", *staticPath)
}
if (!*disableCors) {
e.Use(middleware.CORS())
}
e.Use(middleware.Recover()) e.Use(middleware.Recover())
stdOutLogger.Printf("Connection to %s using macaroon %s and cert %s", *address, *macaroonFile, *certFile)
lndOptions := ln.LNDoptions{ lndOptions := ln.LNDoptions{
Address: *address, Address: *address,
CertFile: *certFile, CertFile: *certFile,
@ -37,16 +44,18 @@ func main() {
} }
lnClient, err := ln.NewLNDclient(lndOptions) lnClient, err := ln.NewLNDclient(lndOptions)
if err != nil { if err != nil {
stdOutLogger.Print("Error initializing LND client:")
panic(err) panic(err)
} }
e.POST("/invoice", func(c echo.Context) error { // endpoint URLs compatible to the LND REST API
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 {
return c.JSON(http.StatusBadRequest, "bad request") return c.JSON(http.StatusBadRequest, "bad request")
} }
invoice, err := lnClient.GenerateInvoice(i.Amount, i.Memo) invoice, err := lnClient.AddInvoice(i.Value, i.Memo)
if err != nil { if err != nil {
return c.JSON(http.StatusInternalServerError, "invoice creation error") return c.JSON(http.StatusInternalServerError, "invoice creation error")
} }
@ -54,9 +63,9 @@ func main() {
return c.JSON(http.StatusOK, invoice) return c.JSON(http.StatusOK, invoice)
}) })
e.GET("/settled/: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, _ := lnClient.CheckInvoice(invoiceId) invoice, _ := lnClient.GetInvoice(invoiceId)
return c.JSON(http.StatusOK, invoice) return c.JSON(http.StatusOK, invoice)
}) })

159
ln/lnd.go
View File

@ -1,9 +1,9 @@
package ln package ln
import ( import (
"context" "context"
"encoding/hex" "encoding/hex"
"io/ioutil" "io/ioutil"
"log" "log"
"os" "os"
@ -11,8 +11,8 @@ import (
"gopkg.in/macaroon.v2" "gopkg.in/macaroon.v2"
"github.com/lightningnetwork/lnd/macaroons" "github.com/lightningnetwork/lnd/macaroons"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/credentials" "google.golang.org/grpc/credentials"
) )
@ -21,84 +21,83 @@ var stdOutLogger = log.New(os.Stdout, "", log.LstdFlags)
// thanks https://github.com/philippgille/ln-paywall/ // thanks https://github.com/philippgille/ln-paywall/
// Invoice is a Lightning Network invoice and contains the typical invoice string and the payment hash. // Invoice is a Lightning Network invoice and contains the typical invoice string and the payment hash.
type Invoice struct { type Invoice struct {
ImplDepID string PaymentHash string `json:"payment_hash"`
PaymentHash string PaymentRequest string `json:"payment_request"`
PaymentRequest string Settled bool `json:"settled"`
} }
type LNDclient struct { type LNDclient struct {
lndClient lnrpc.LightningClient lndClient lnrpc.LightningClient
ctx context.Context ctx context.Context
conn *grpc.ClientConn conn *grpc.ClientConn
} }
// GenerateInvoice generates an invoice with the given price and memo. // AddInvoice generates an invoice with the given price and memo.
func (c LNDclient) GenerateInvoice(amount int64, memo string) (Invoice, error) { func (c LNDclient) AddInvoice(value int64, memo string) (Invoice, error) {
result := Invoice{} result := Invoice{}
stdOutLogger.Printf("Creating invoice: memo=%s amount=%v ", memo, amount) stdOutLogger.Printf("Creating invoice: memo=%s amount=%v ", memo, value)
invoice := lnrpc.Invoice{ invoice := lnrpc.Invoice{
Memo: memo, Memo: memo,
Value: amount, Value: value,
} }
res, err := c.lndClient.AddInvoice(c.ctx, &invoice) res, err := c.lndClient.AddInvoice(c.ctx, &invoice)
if err != nil { if err != nil {
return result, err return result, err
} }
result.ImplDepID = hex.EncodeToString(res.RHash) result.PaymentHash = hex.EncodeToString(res.RHash)
result.PaymentHash = result.ImplDepID result.PaymentRequest = res.PaymentRequest
result.PaymentRequest = res.PaymentRequest return result, nil
return result, nil
} }
// CheckInvoice takes an invoice ID and checks if the corresponding invoice was settled. // GetInvoice takes an invoice ID and returns the invoice details including settlement details
// An error is returned if no corresponding invoice was found. // An error is returned if no corresponding invoice was found.
// False is returned if the invoice isn't settled. func (c LNDclient) GetInvoice(paymentHashStr string) (Invoice, error) {
func (c LNDclient) CheckInvoice(id string) (bool, error) { var invoice Invoice
// In the case of lnd, the ID is the hex encoded preimage hash. stdOutLogger.Printf("Lookup invoice: hash=%s\n", paymentHashStr)
plainHash, err := hex.DecodeString(id)
if err != nil {
return false, err
}
stdOutLogger.Printf("Lookup invoice: hash=%s\n", id) plainHash, err := hex.DecodeString(paymentHashStr)
if err != nil {
return invoice, err
}
// Get the invoice for that hash // Get the invoice for that hash
paymentHash := lnrpc.PaymentHash{ paymentHash := lnrpc.PaymentHash{
RHash: plainHash, RHash: plainHash,
// Hex encoded, must be exactly 32 byte // Hex encoded, must be exactly 32 byte
RHashStr: id, RHashStr: paymentHashStr,
} }
invoice, err := c.lndClient.LookupInvoice(c.ctx, &paymentHash) result, err := c.lndClient.LookupInvoice(c.ctx, &paymentHash)
if err != nil { if err != nil {
return false, err return invoice, err
} }
// Check if invoice was settled invoice = Invoice{}
if !invoice.GetSettled() { invoice.PaymentHash = hex.EncodeToString(result.RHash)
return false, nil invoice.PaymentRequest = result.PaymentRequest
} invoice.Settled = result.GetSettled()
return true, nil
return invoice, nil
} }
func NewLNDclient(lndOptions LNDoptions) (LNDclient, error) { func NewLNDclient(lndOptions LNDoptions) (LNDclient, error) {
result := LNDclient{} result := LNDclient{}
creds, err := credentials.NewClientTLSFromFile(lndOptions.CertFile, "") creds, err := credentials.NewClientTLSFromFile(lndOptions.CertFile, "")
if err != nil { if err != nil {
return result, err return result, err
} }
opts := []grpc.DialOption{ opts := []grpc.DialOption{
grpc.WithTransportCredentials(creds), grpc.WithTransportCredentials(creds),
} }
macaroonData, err := ioutil.ReadFile(lndOptions.MacaroonFile) macaroonData, err := ioutil.ReadFile(lndOptions.MacaroonFile)
if err != nil { if err != nil {
return result, err return result, err
} }
mac := &macaroon.Macaroon{} mac := &macaroon.Macaroon{}
if err = mac.UnmarshalBinary(macaroonData); err != nil { if err = mac.UnmarshalBinary(macaroonData); err != nil {
return result, err return result, err
} }
@ -110,30 +109,30 @@ func NewLNDclient(lndOptions LNDoptions) (LNDclient, error) {
return result, err return result, err
} }
c := lnrpc.NewLightningClient(conn) c := lnrpc.NewLightningClient(conn)
result = LNDclient{ result = LNDclient{
conn: conn, conn: conn,
ctx: context.Background(), ctx: context.Background(),
lndClient: c, lndClient: c,
} }
return result, nil return result, nil
} }
// LNDoptions are the options for the connection to the lnd node. // LNDoptions are the options for the connection to the lnd node.
type LNDoptions struct { type LNDoptions struct {
// Address of your LND node, including the port. // Address of your LND node, including the port.
// Optional ("localhost:10009" by default). // Optional ("localhost:10009" by default).
Address string Address string
// Path to the "tls.cert" file that your LND node uses. // Path to the "tls.cert" file that your LND node uses.
// Optional ("tls.cert" by default). // Optional ("tls.cert" by default).
CertFile string CertFile string
// Path to the macaroon file that your LND node uses. // Path to the macaroon file that your LND node uses.
// "invoice.macaroon" if you only use the GenerateInvoice() and CheckInvoice() methods // "invoice.macaroon" if you only use the AddInvoice() and GetInvoice() methods
// (required by the middleware in the package "wall"). // (required by the middleware in the package "wall").
// "admin.macaroon" if you use the Pay() method (required by the client in the package "pay"). // "admin.macaroon" if you use the Pay() method (required by the client in the package "pay").
// Optional ("invoice.macaroon" by default). // Optional ("invoice.macaroon" by default).
MacaroonFile string MacaroonFile string
} }