1
1
mirror of https://github.com/bumi/lntip synced 2025-07-03 07:42:03 +00:00

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) => {
if (response.ok) {
return response.json(); 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.Static("/static", *staticPath)
}
if (!*disableCors) {
e.Use(middleware.CORS()) 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)
}) })

View File

@ -21,9 +21,9 @@ 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 {
@ -32,54 +32,53 @@ type LNDclient struct {
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) {
@ -130,7 +129,7 @@ type LNDoptions struct {
// 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).