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
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
new LnTip({ amount: 1000, memo: 'high5' }).request()
new LnTip({ value: 1000, memo: 'high5' }).request()
```
Use it from a plain HTML link:
```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:
```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
// promise resolves if the invoice is settled
@ -73,7 +73,7 @@ tip.requestPayment().then((invoice) => {
});
// create a new invoice
tip.getInvoice().then((invoice) => {
tip.addInvoice().then((invoice) => {
console.log(invoice.PaymentRequest)
});
@ -84,6 +84,9 @@ tip.watchPayment().then((invoice) => {
```
## Development
## Contributing

View File

@ -4,7 +4,7 @@
LnTip = function (options) {
var host = document.getElementById('lntip-script').getAttribute('lntip-host');
this.host = options.host || host;
this.amount = options.amount;
this.value = options.value;
this.memo = options.memo || '';
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) => {
this.paymentWatcher = window.setInterval(() => {
this._fetch(`${this.host}/settled/${this.invoice.ImplDepID}`)
.then((settled) => {
if (settled) {
this._fetch(`${this.host}/v1/invoice/${this.invoice.ImplDepID}`)
.then((invoice) => {
if (invoice.settled) {
this.invoice.settled = true;
this.stopWatchingPayment();
resolve(this.invoice);
@ -72,25 +72,25 @@ LnTip.prototype.stopWatchingPayment = function () {
LnTip.prototype.payWithWebln = function () {
if (!webln.isEnabled) {
webln.enable().then((weblnResponse) => {
return webln.sendPayment({ paymentRequest: this.invoice.PaymentRequest })
return webln.sendPayment({ paymentRequest: this.invoice.payment_request })
}).catch((e) => {
return this.showPaymentRequest();
})
} else {
return webln.sendPayment({ paymentRequest: this.invoice.PaymentRequest })
return webln.sendPayment({ paymentRequest: this.invoice.payment_request })
}
}
LnTip.prototype.showPaymentRequest = function () {
var content = `<div class="lntip-payment-request">
<h1>${this.memo}</h1>
<h2>${this.amount} satoshi</h2>
<h2>${this.value} satoshi</h2>
<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 class="lntip-details">
<a href="lightning:${this.invoice.PaymentRequest}" class="lntip-invoice">
${this.invoice.PaymentRequest}
<a href="lightning:${this.invoice.payment_request}" class="lntip-invoice">
${this.invoice.payment_request}
</a>
<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>
@ -100,30 +100,30 @@ LnTip.prototype.showPaymentRequest = function () {
this.openPopup(content);
document.getElementById('lntip-copy').onclick = () => {
navigator.clipboard.writeText(this.invoice.PaymentRequest);
navigator.clipboard.writeText(this.invoice.payment_request);
alert('Copied to clipboad');
}
return Promise.resolve(); // be compatible to payWithWebln()
}
LnTip.prototype.getInvoice = function () {
LnTip.prototype.addInvoice = function () {
var args = {
method: 'POST',
mode: 'cors',
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(
`${this.host}/invoice`,
`${this.host}/v1/invoices`,
args
).then((invoice) => {
this.invoice = invoice;
return invoice;
})
});
}
LnTip.prototype.requestPayment = function () {
return this.getInvoice().then((invoice) => {
return this.addInvoice().then((invoice) => {
if (typeof webln !== 'undefined') {
return this.payWithWebln();
} else {
@ -142,6 +142,10 @@ LnTip.prototype.request = function () {
LnTip.prototype._fetch = function(url, args) {
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>
</head>
<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>
</html>

View File

@ -13,7 +13,7 @@ import (
var stdOutLogger = log.New(os.Stdout, "", log.LstdFlags)
type Invoice struct {
Amount int64 `json:"amount"`
Value int64 `json:"value"`
Memo string `json:"memo"`
}
@ -22,14 +22,21 @@ func main() {
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")
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()
e := echo.New()
e.Static("/static", "assets")
e.Use(middleware.CORS())
if (*staticPath != "") {
e.Static("/static", *staticPath)
}
if (!*disableCors) {
e.Use(middleware.CORS())
}
e.Use(middleware.Recover())
stdOutLogger.Printf("Connection to %s using macaroon %s and cert %s", *address, *macaroonFile, *certFile)
lndOptions := ln.LNDoptions{
Address: *address,
CertFile: *certFile,
@ -37,16 +44,18 @@ func main() {
}
lnClient, err := ln.NewLNDclient(lndOptions)
if err != nil {
stdOutLogger.Print("Error initializing LND client:")
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)
if err := c.Bind(i); err != nil {
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 {
return c.JSON(http.StatusInternalServerError, "invoice creation error")
}
@ -54,9 +63,9 @@ func main() {
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")
invoice, _ := lnClient.CheckInvoice(invoiceId)
invoice, _ := lnClient.GetInvoice(invoiceId)
return c.JSON(http.StatusOK, invoice)
})

159
ln/lnd.go
View File

@ -1,9 +1,9 @@
package ln
import (
"context"
"encoding/hex"
"io/ioutil"
"context"
"encoding/hex"
"io/ioutil"
"log"
"os"
@ -11,8 +11,8 @@ import (
"gopkg.in/macaroon.v2"
"github.com/lightningnetwork/lnd/macaroons"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)
@ -21,84 +21,83 @@ var stdOutLogger = log.New(os.Stdout, "", log.LstdFlags)
// thanks https://github.com/philippgille/ln-paywall/
// Invoice is a Lightning Network invoice and contains the typical invoice string and the payment hash.
type Invoice struct {
ImplDepID string
PaymentHash string
PaymentRequest string
PaymentHash string `json:"payment_hash"`
PaymentRequest string `json:"payment_request"`
Settled bool `json:"settled"`
}
type LNDclient struct {
lndClient lnrpc.LightningClient
ctx context.Context
conn *grpc.ClientConn
lndClient lnrpc.LightningClient
ctx context.Context
conn *grpc.ClientConn
}
// GenerateInvoice generates an invoice with the given price and memo.
func (c LNDclient) GenerateInvoice(amount int64, memo string) (Invoice, error) {
result := Invoice{}
// AddInvoice generates an invoice with the given price and memo.
func (c LNDclient) AddInvoice(value int64, memo string) (Invoice, error) {
result := Invoice{}
stdOutLogger.Printf("Creating invoice: memo=%s amount=%v ", memo, amount)
invoice := lnrpc.Invoice{
Memo: memo,
Value: amount,
}
res, err := c.lndClient.AddInvoice(c.ctx, &invoice)
if err != nil {
return result, err
}
stdOutLogger.Printf("Creating invoice: memo=%s amount=%v ", memo, value)
invoice := lnrpc.Invoice{
Memo: memo,
Value: value,
}
res, err := c.lndClient.AddInvoice(c.ctx, &invoice)
if err != nil {
return result, err
}
result.ImplDepID = hex.EncodeToString(res.RHash)
result.PaymentHash = result.ImplDepID
result.PaymentRequest = res.PaymentRequest
return result, nil
result.PaymentHash = hex.EncodeToString(res.RHash)
result.PaymentRequest = res.PaymentRequest
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.
// False is returned if the invoice isn't settled.
func (c LNDclient) CheckInvoice(id string) (bool, error) {
// In the case of lnd, the ID is the hex encoded preimage hash.
plainHash, err := hex.DecodeString(id)
if err != nil {
return false, err
}
func (c LNDclient) GetInvoice(paymentHashStr string) (Invoice, error) {
var invoice Invoice
stdOutLogger.Printf("Lookup invoice: hash=%s\n", paymentHashStr)
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
paymentHash := lnrpc.PaymentHash{
RHash: plainHash,
// Hex encoded, must be exactly 32 byte
RHashStr: id,
}
invoice, err := c.lndClient.LookupInvoice(c.ctx, &paymentHash)
if err != nil {
return false, err
}
// Get the invoice for that hash
paymentHash := lnrpc.PaymentHash{
RHash: plainHash,
// Hex encoded, must be exactly 32 byte
RHashStr: paymentHashStr,
}
result, err := c.lndClient.LookupInvoice(c.ctx, &paymentHash)
if err != nil {
return invoice, err
}
// Check if invoice was settled
if !invoice.GetSettled() {
return false, nil
}
return true, nil
invoice = Invoice{}
invoice.PaymentHash = hex.EncodeToString(result.RHash)
invoice.PaymentRequest = result.PaymentRequest
invoice.Settled = result.GetSettled()
return invoice, nil
}
func NewLNDclient(lndOptions LNDoptions) (LNDclient, error) {
result := LNDclient{}
result := LNDclient{}
creds, err := credentials.NewClientTLSFromFile(lndOptions.CertFile, "")
if err != nil {
return result, err
}
creds, err := credentials.NewClientTLSFromFile(lndOptions.CertFile, "")
if err != nil {
return result, err
}
opts := []grpc.DialOption{
grpc.WithTransportCredentials(creds),
}
macaroonData, err := ioutil.ReadFile(lndOptions.MacaroonFile)
if err != nil {
return result, err
}
macaroonData, err := ioutil.ReadFile(lndOptions.MacaroonFile)
if err != nil {
return result, err
}
mac := &macaroon.Macaroon{}
if err = mac.UnmarshalBinary(macaroonData); err != nil {
if err = mac.UnmarshalBinary(macaroonData); err != nil {
return result, err
}
@ -110,30 +109,30 @@ func NewLNDclient(lndOptions LNDoptions) (LNDclient, error) {
return result, err
}
c := lnrpc.NewLightningClient(conn)
c := lnrpc.NewLightningClient(conn)
result = LNDclient{
conn: conn,
ctx: context.Background(),
lndClient: c,
}
result = LNDclient{
conn: conn,
ctx: context.Background(),
lndClient: c,
}
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 GenerateInvoice() and CheckInvoice() 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
// 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
}