mirror of https://github.com/bumi/lntip
Refactor to mimic the lnd rest API a bit
This commit is contained in:
parent
d3e7dc0668
commit
01fb7be447
13
README.md
13
README.md
|
@ -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
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
159
ln/lnd.go
|
@ -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
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue