diff --git a/README.md b/README.md index 7a2a45f..60ad5ac 100644 --- a/README.md +++ b/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 - Tip me + Tip me ``` ##### 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 diff --git a/assets/lntip.js b/assets/lntip.js index b73ddfa..d261516 100644 --- a/assets/lntip.js +++ b/assets/lntip.js @@ -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 = `

${this.memo}

-

${this.amount} satoshi

+

${this.value} satoshi

- +
- - ${this.invoice.PaymentRequest} + + ${this.invoice.payment_request}
@@ -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); + } }) } diff --git a/examples/tipping.html b/examples/tipping.html index d5a6b83..bbd7c36 100644 --- a/examples/tipping.html +++ b/examples/tipping.html @@ -5,8 +5,8 @@ - + - Tip me + Tip me diff --git a/invoices.go b/invoices_proxy.go similarity index 62% rename from invoices.go rename to invoices_proxy.go index 0076f6f..4ddcc6c 100644 --- a/invoices.go +++ b/invoices_proxy.go @@ -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) }) diff --git a/ln/lnd.go b/ln/lnd.go index b75d4d4..afdb01b 100644 --- a/ln/lnd.go +++ b/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 }