diff --git a/backends/lnd.go b/backends/lnd.go index cd65fa2..be27a7b 100644 --- a/backends/lnd.go +++ b/backends/lnd.go @@ -3,10 +3,13 @@ package backends import ( "context" "encoding/hex" + "errors" + "fmt" "github.com/lightningnetwork/lnd/lnrpc" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "google.golang.org/grpc/metadata" + "io" "io/ioutil" ) @@ -76,3 +79,41 @@ func (lnd *LND) GetInvoice(description string, value int64, expiry int64) (invoi return response.PaymentRequest, err } + +func (lnd *LND) SubscribeInvoices() error { + stream, err := lnd.client.SubscribeInvoices(lnd.ctx, &lnrpc.InvoiceSubscription{}) + + if err != nil { + return err + } + + wait := make(chan struct{}) + + go func() { + for { + in, streamErr := stream.Recv() + + if streamErr == io.EOF { + err = errors.New("lost connection to LND gRPC") + + close(wait) + + return + } + + if streamErr != nil { + err = streamErr + + close(wait) + + return + } + + fmt.Println(in) + } + }() + + <-wait + + return err +} diff --git a/config.go b/config.go index 2096fd1..435ebba 100644 --- a/config.go +++ b/config.go @@ -10,12 +10,20 @@ import ( "strings" ) +// TODO: add option to show URI of Lightning node + const ( defaultConfigFile = "lightningTip.conf" defaultLogFile = "lightningTip.log" defaultLogLevel = "debug" + defaultRESTHost = "localhost:8081" + + defaultTipMessage = "A generous tip" + defaultTipExpiry = 3600 + defaultTipValue = 100 + defaultLndRPCHost = "localhost:10009" defaultLndCertFile = "tls.cert" defaultMacaroonFile = "admin.macaroon" @@ -27,13 +35,18 @@ type config struct { LogFile string `long:"logfile" Description:"Log file location"` LogLevel string `long:"loglevel" Description:"Log level: debug, info, warning, error"` + RESTHost string `long:"resthost" Description:"Host for the rest interface of LightningTip"` + + TipMessage string `long:"tipmessage" Description:"Message embedded in the payment request"` + TipExpiry int64 `long:"tipexpiry" Description:"Expiry time in seconds"` + DefaultTipValue int64 `long:"defaulttipvalue" Description:"The default value of a tip in satoshis"` + LND *backends.LND `group:"LND" namespace:"lnd"` } var cfg config var backend backends.Backend -var backendName string func initConfig() { lndDir := getDefaultLndDir() @@ -44,6 +57,12 @@ func initConfig() { LogFile: defaultLogFile, LogLevel: defaultLogLevel, + RESTHost: defaultRESTHost, + + TipMessage: defaultTipMessage, + TipExpiry: defaultTipExpiry, + DefaultTipValue: defaultTipValue, + LND: &backends.LND{ RPCHost: defaultLndRPCHost, CertFile: path.Join(lndDir, defaultLndCertFile), @@ -82,9 +101,8 @@ func initConfig() { log.Infof("Failed to parse config file: %v", errFile) } - // TODO: add more backend options like for example c-lighting + // TODO: add more backend options like for example c-lighting and eclair backend = cfg.LND - backendName = "LND" } func getDefaultLndDir() (dir string) { diff --git a/lightningtip.go b/lightningtip.go index fb0e2c6..9fe024d 100644 --- a/lightningtip.go +++ b/lightningtip.go @@ -1,6 +1,31 @@ package main -import "fmt" +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "os" + "strconv" +) + +type getInvoiceRequest struct { + Value int64 + Message string +} + +type invoiceResponse struct { + Invoice string + Error string +} + +type tipValueResponse struct { + TipValue int64 +} + +type errorResponse struct { + Error string +} func main() { initLog() @@ -10,12 +35,108 @@ func main() { err := backend.Connect() if err == nil { - log.Info("Successfully connected to " + backendName) + http.HandleFunc("/", notFoundHandler) + http.HandleFunc("/getinvoice", getInvoiceHandler) + http.HandleFunc("/defaulttipvalue", defaultTipValueHandler) - invoice, err := backend.GetInvoice("Just a test", 1, 3600) + log.Info("Subscribing to invoices") + + go func() { + err = cfg.LND.SubscribeInvoices() + + 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 {} - log.Info("Got invoice " + invoice) - log.Info(fmt.Sprint(err)) } } + +func getInvoiceHandler(writer http.ResponseWriter, request *http.Request) { + var errorMessage string + + tipValue := cfg.DefaultTipValue + tipMessage := cfg.TipMessage + + if request.Method == http.MethodPost { + var body getInvoiceRequest + + data, _ := ioutil.ReadAll(request.Body) + + err := json.Unmarshal(data, &body) + + if err == nil { + if body.Value != 0 { + tipValue = body.Value + } + + if body.Message != "" { + tipMessage = body.Message + } + + } else { + errorMessage = "Could not parse values from request" + + log.Warning(errorMessage) + } + + } + + invoice, err := backend.GetInvoice(tipMessage, tipValue, cfg.TipExpiry) + + if err == nil { + log.Info("Created invoice with value of " + strconv.FormatInt(tipValue, 10) + " satoshis") + + writer.Write(marshalJson(invoiceResponse{ + Invoice: invoice, + Error: errorMessage, + })) + + } else { + errorMessage := "Failed to create invoice" + + log.Error(errorMessage + ": " + fmt.Sprint(err)) + + writer.Write(marshalJson(errorResponse{ + Error: errorMessage, + })) + } + +} + +func defaultTipValueHandler(writer http.ResponseWriter, request *http.Request) { + writer.Write(marshalJson(tipValueResponse{ + TipValue: cfg.DefaultTipValue, + })) +} + +func notFoundHandler(writer http.ResponseWriter, request *http.Request) { + writer.Write(marshalJson(errorResponse{ + Error: "Not found", + })) +} + +func marshalJson(data interface{}) []byte { + response, _ := json.MarshalIndent(data, "", " ") + + return response +}