2018-03-22 11:03:54 -07:00
|
|
|
package main
|
|
|
|
|
2018-03-24 15:03:55 -07:00
|
|
|
import (
|
2018-03-26 14:54:12 -07:00
|
|
|
"crypto/sha256"
|
|
|
|
"encoding/hex"
|
2018-03-24 15:03:55 -07:00
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
2018-03-26 14:54:12 -07:00
|
|
|
"github.com/donovanhide/eventsource"
|
2018-03-24 15:03:55 -07:00
|
|
|
"io/ioutil"
|
|
|
|
"net/http"
|
|
|
|
"os"
|
|
|
|
"strconv"
|
2018-03-26 14:54:12 -07:00
|
|
|
"time"
|
2018-03-24 15:03:55 -07:00
|
|
|
)
|
|
|
|
|
2018-03-26 14:54:12 -07:00
|
|
|
const eventChannel = "invoiceSettled"
|
|
|
|
|
2018-03-28 09:30:19 -07:00
|
|
|
const couldNotParseError = "Could not parse values from request"
|
|
|
|
|
2018-03-26 14:54:12 -07:00
|
|
|
var eventSrv *eventsource.Server
|
|
|
|
|
|
|
|
var pendingInvoices []PendingInvoice
|
|
|
|
|
|
|
|
type PendingInvoice struct {
|
|
|
|
Invoice string
|
|
|
|
Hash string
|
|
|
|
Expiry time.Time
|
|
|
|
}
|
|
|
|
|
|
|
|
// To use the pendingInvoice type as event for the EventSource stream
|
|
|
|
func (pending PendingInvoice) Id() string { return "" }
|
|
|
|
func (pending PendingInvoice) Event() string { return "" }
|
|
|
|
func (pending PendingInvoice) Data() string { return pending.Hash }
|
|
|
|
|
2018-03-28 09:30:19 -07:00
|
|
|
type invoiceRequest struct {
|
|
|
|
Amount int64
|
|
|
|
Message string
|
|
|
|
}
|
|
|
|
|
2018-03-24 15:03:55 -07:00
|
|
|
type invoiceResponse struct {
|
|
|
|
Invoice string
|
2018-03-25 08:45:42 -07:00
|
|
|
Expiry int64
|
2018-03-24 15:03:55 -07:00
|
|
|
}
|
|
|
|
|
2018-03-28 09:30:19 -07:00
|
|
|
type invoiceSettledRequest struct {
|
|
|
|
InvoiceHash string
|
|
|
|
}
|
|
|
|
|
|
|
|
type invoiceSettledResponse struct {
|
|
|
|
Settled bool
|
2018-03-24 15:03:55 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
type errorResponse struct {
|
|
|
|
Error string
|
|
|
|
}
|
2018-03-23 16:08:03 -07:00
|
|
|
|
2018-03-25 08:45:42 -07:00
|
|
|
// TODO: add option to show URI of Lightning node
|
2018-03-22 11:03:54 -07:00
|
|
|
func main() {
|
|
|
|
initLog()
|
2018-03-22 13:52:16 -07:00
|
|
|
|
|
|
|
initConfig()
|
2018-03-23 16:08:03 -07:00
|
|
|
|
|
|
|
err := backend.Connect()
|
|
|
|
|
|
|
|
if err == nil {
|
2018-03-26 14:54:12 -07:00
|
|
|
log.Info("Starting EventSource stream")
|
|
|
|
|
|
|
|
eventSrv = eventsource.NewServer()
|
|
|
|
|
2018-03-29 04:44:08 -07:00
|
|
|
http.Handle("/", handleHeaders(notFoundHandler))
|
|
|
|
http.Handle("/getinvoice", handleHeaders(getInvoiceHandler))
|
|
|
|
http.Handle("/eventsource", handleHeaders(eventSrv.Handler(eventChannel)))
|
2018-03-24 15:03:55 -07:00
|
|
|
|
2018-03-28 09:30:19 -07:00
|
|
|
// Alternative for browsers which don't support EventSource (Internet Explorer and Edge)
|
2018-03-29 04:44:08 -07:00
|
|
|
http.Handle("/invoicesettled", handleHeaders(invoiceSettledHandler))
|
2018-03-28 09:30:19 -07:00
|
|
|
|
2018-03-29 04:06:28 -07:00
|
|
|
log.Debug("Starting ticker to clear expired invoices")
|
|
|
|
|
|
|
|
// A bit longer than the expiry time to make sure the invoice doesn't show as settled if it isn't (affects just invoiceSettledHandler)
|
|
|
|
duration := time.Duration(cfg.TipExpiry + 10)
|
|
|
|
ticker := time.Tick(duration * time.Second)
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case <-ticker:
|
|
|
|
now := time.Now()
|
|
|
|
|
|
|
|
for index := len(pendingInvoices) - 1; index >= 0; index-- {
|
|
|
|
invoice := pendingInvoices[index]
|
|
|
|
|
|
|
|
if now.Sub(invoice.Expiry) > 0 {
|
|
|
|
log.Debug("Invoice expired: " + invoice.Invoice)
|
|
|
|
|
|
|
|
pendingInvoices = append(pendingInvoices[:index], pendingInvoices[index+1:]...)
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}()
|
|
|
|
|
2018-03-24 15:03:55 -07:00
|
|
|
log.Info("Subscribing to invoices")
|
|
|
|
|
|
|
|
go func() {
|
2018-03-26 14:54:12 -07:00
|
|
|
err = cfg.LND.SubscribeInvoices(publishInvoiceSettled, eventSrv)
|
2018-03-24 15:03:55 -07:00
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
log.Error("Failed to subscribe to invoices: " + fmt.Sprint(err))
|
|
|
|
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
log.Info("Starting HTTP server")
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
err = http.ListenAndServe(cfg.RESTHost, nil)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
log.Error("Failed to start HTTP server: " + fmt.Sprint(err))
|
|
|
|
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
select {}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2018-03-28 09:30:19 -07:00
|
|
|
// Callback for backend
|
2018-03-26 14:54:12 -07:00
|
|
|
func publishInvoiceSettled(invoice string, eventSrv *eventsource.Server) {
|
|
|
|
for index, pending := range pendingInvoices {
|
|
|
|
if pending.Invoice == invoice {
|
|
|
|
log.Info("Invoice settled: " + invoice)
|
|
|
|
|
|
|
|
eventSrv.Publish([]string{eventChannel}, pending)
|
|
|
|
|
|
|
|
pendingInvoices = append(pendingInvoices[:index], pendingInvoices[index+1:]...)
|
|
|
|
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2018-03-28 09:30:19 -07:00
|
|
|
func invoiceSettledHandler(writer http.ResponseWriter, request *http.Request) {
|
|
|
|
errorMessage := couldNotParseError
|
|
|
|
|
|
|
|
if request.Method == http.MethodPost {
|
|
|
|
var body invoiceSettledRequest
|
|
|
|
|
|
|
|
data, _ := ioutil.ReadAll(request.Body)
|
|
|
|
|
|
|
|
err := json.Unmarshal(data, &body)
|
|
|
|
|
|
|
|
if err == nil {
|
|
|
|
if body.InvoiceHash != "" {
|
|
|
|
settled := true
|
|
|
|
|
|
|
|
for _, pending := range pendingInvoices {
|
|
|
|
if pending.Hash == body.InvoiceHash {
|
|
|
|
settled = false
|
|
|
|
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
writer.Write(marshalJson(invoiceSettledResponse{
|
|
|
|
Settled: settled,
|
|
|
|
}))
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Error(errorMessage)
|
|
|
|
|
|
|
|
writeError(writer, errorMessage)
|
|
|
|
}
|
|
|
|
|
2018-03-24 15:03:55 -07:00
|
|
|
func getInvoiceHandler(writer http.ResponseWriter, request *http.Request) {
|
2018-03-28 09:30:19 -07:00
|
|
|
errorMessage := couldNotParseError
|
2018-03-24 15:03:55 -07:00
|
|
|
|
|
|
|
if request.Method == http.MethodPost {
|
2018-03-25 08:45:42 -07:00
|
|
|
var body invoiceRequest
|
2018-03-24 15:03:55 -07:00
|
|
|
|
|
|
|
data, _ := ioutil.ReadAll(request.Body)
|
|
|
|
|
|
|
|
err := json.Unmarshal(data, &body)
|
|
|
|
|
|
|
|
if err == nil {
|
2018-03-25 08:45:42 -07:00
|
|
|
if body.Amount != 0 {
|
|
|
|
invoice, err := backend.GetInvoice(body.Message, body.Amount, cfg.TipExpiry)
|
2018-03-24 15:03:55 -07:00
|
|
|
|
2018-03-25 08:45:42 -07:00
|
|
|
if err == nil {
|
|
|
|
logMessage := "Created invoice with amount of " + strconv.FormatInt(body.Amount, 10) + " satoshis"
|
2018-03-24 15:03:55 -07:00
|
|
|
|
2018-03-25 08:45:42 -07:00
|
|
|
if body.Message != "" {
|
|
|
|
logMessage += " with message \"" + body.Message + "\""
|
|
|
|
}
|
2018-03-24 15:03:55 -07:00
|
|
|
|
2018-03-26 14:54:12 -07:00
|
|
|
sha := sha256.New()
|
|
|
|
sha.Write([]byte(invoice))
|
|
|
|
|
|
|
|
hash := hex.EncodeToString(sha.Sum(nil))
|
|
|
|
|
|
|
|
expiryDuration := time.Duration(cfg.TipExpiry) * time.Second
|
|
|
|
|
2018-03-25 08:45:42 -07:00
|
|
|
log.Info(logMessage)
|
2018-03-24 15:03:55 -07:00
|
|
|
|
2018-03-26 14:54:12 -07:00
|
|
|
pendingInvoices = append(pendingInvoices, PendingInvoice{
|
|
|
|
Invoice: invoice,
|
|
|
|
Hash: hash,
|
|
|
|
Expiry: time.Now().Add(expiryDuration),
|
|
|
|
})
|
|
|
|
|
2018-03-25 08:45:42 -07:00
|
|
|
writer.Write(marshalJson(invoiceResponse{
|
|
|
|
Invoice: invoice,
|
|
|
|
Expiry: cfg.TipExpiry,
|
|
|
|
}))
|
2018-03-24 15:03:55 -07:00
|
|
|
|
2018-03-25 08:45:42 -07:00
|
|
|
return
|
2018-03-24 15:03:55 -07:00
|
|
|
|
2018-03-25 08:45:42 -07:00
|
|
|
} else {
|
|
|
|
errorMessage = "Failed to create invoice"
|
|
|
|
}
|
2018-03-23 16:08:03 -07:00
|
|
|
|
2018-03-25 08:45:42 -07:00
|
|
|
}
|
2018-03-23 16:08:03 -07:00
|
|
|
|
2018-03-25 08:45:42 -07:00
|
|
|
}
|
2018-03-24 15:03:55 -07:00
|
|
|
|
2018-03-23 16:08:03 -07:00
|
|
|
}
|
|
|
|
|
2018-03-25 08:45:42 -07:00
|
|
|
log.Error(errorMessage)
|
2018-03-24 15:03:55 -07:00
|
|
|
|
2018-03-25 08:45:42 -07:00
|
|
|
writeError(writer, errorMessage)
|
2018-03-24 15:03:55 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
func notFoundHandler(writer http.ResponseWriter, request *http.Request) {
|
2018-03-25 08:45:42 -07:00
|
|
|
writeError(writer, "Not found")
|
|
|
|
}
|
|
|
|
|
2018-03-29 04:44:08 -07:00
|
|
|
func handleHeaders(handler func(w http.ResponseWriter, r *http.Request)) http.Handler {
|
|
|
|
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
|
|
|
if cfg.AccessDomain != "" {
|
|
|
|
writer.Header().Add("Access-Control-Allow-Origin", cfg.AccessDomain)
|
|
|
|
}
|
|
|
|
|
|
|
|
handler(writer, request)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2018-03-25 08:45:42 -07:00
|
|
|
func writeError(writer http.ResponseWriter, message string) {
|
|
|
|
writer.WriteHeader(http.StatusBadRequest)
|
|
|
|
|
2018-03-24 15:03:55 -07:00
|
|
|
writer.Write(marshalJson(errorResponse{
|
2018-03-25 08:45:42 -07:00
|
|
|
Error: message,
|
2018-03-24 15:03:55 -07:00
|
|
|
}))
|
|
|
|
}
|
|
|
|
|
|
|
|
func marshalJson(data interface{}) []byte {
|
|
|
|
response, _ := json.MarshalIndent(data, "", " ")
|
|
|
|
|
|
|
|
return response
|
|
|
|
}
|