diff --git a/backends/backend.go b/backends/backend.go new file mode 100644 index 0000000..373e9cf --- /dev/null +++ b/backends/backend.go @@ -0,0 +1,8 @@ +package backends + +type Backend interface { + Connect() error + + // Value in satoshis and expiry in seconds + GetInvoice(description string, value int64, expiry int64) (invoice string, err error) +} diff --git a/backends/lnd.go b/backends/lnd.go new file mode 100644 index 0000000..cd65fa2 --- /dev/null +++ b/backends/lnd.go @@ -0,0 +1,78 @@ +package backends + +import ( + "context" + "encoding/hex" + "github.com/lightningnetwork/lnd/lnrpc" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/metadata" + "io/ioutil" +) + +type LND struct { + RPCHost string `long:"rpchost" Description:"Host that the gRPC interface of LND is listening to"` + CertFile string `long:"certfile" Description:"TLS certificate for LND gRPC and REST services"` + MacaroonFile string `long:"macaroonfile" Description:"Admin macaroon file for LND authentication. Set to an empty string for no macaroon"` + + client lnrpc.LightningClient + ctx context.Context +} + +func (lnd *LND) Connect() error { + creds, err := credentials.NewClientTLSFromFile(lnd.CertFile, "") + + if err != nil { + log.Error("Failed to read certificate for LND gRPC") + + return err + } + + con, err := grpc.Dial(lnd.RPCHost, grpc.WithTransportCredentials(creds)) + + if err != nil { + log.Error("Failed to connect to LND gRPC server") + + return err + } + + lnd.ctx = context.Background() + + if lnd.MacaroonFile != "" { + macaroon, err := getMacaroon(lnd.MacaroonFile) + + if macaroon == nil && err != nil { + log.Error("Failed to read admin macaroon file of LND") + } + + lnd.ctx = metadata.NewOutgoingContext(lnd.ctx, macaroon) + } + + lnd.client = lnrpc.NewLightningClient(con) + + return err +} + +func getMacaroon(macaroonFile string) (macaroon metadata.MD, err error) { + data, err := ioutil.ReadFile(macaroonFile) + + if err == nil { + macaroon = metadata.Pairs("macaroon", hex.EncodeToString(data)) + } + + return macaroon, err +} + +func (lnd *LND) GetInvoice(description string, value int64, expiry int64) (invoice string, err error) { + response, err := lnd.client.AddInvoice(lnd.ctx, &lnrpc.Invoice{ + Memo: description, + Value: value, + Expiry: expiry, + }) + + if err != nil { + return "", err + } + + return response.PaymentRequest, err +} diff --git a/backends/log.go b/backends/log.go new file mode 100644 index 0000000..74e1fb7 --- /dev/null +++ b/backends/log.go @@ -0,0 +1,9 @@ +package backends + +import "github.com/op/go-logging" + +var log logging.Logger + +func UseLogger(logger logging.Logger) { + log = logger +} diff --git a/config.go b/config.go index 37617c4..2096fd1 100644 --- a/config.go +++ b/config.go @@ -2,7 +2,11 @@ package main import ( "github.com/jessevdk/go-flags" + "github.com/michael1011/lightningtip/backends" "github.com/op/go-logging" + "os/user" + "path" + "runtime" "strings" ) @@ -11,23 +15,40 @@ const ( defaultLogFile = "lightningTip.log" defaultLogLevel = "debug" + + defaultLndRPCHost = "localhost:10009" + defaultLndCertFile = "tls.cert" + defaultMacaroonFile = "admin.macaroon" ) type config struct { - ConfigFile string `long:"config" Description:"Config file location"` + ConfigFile string `long:"config" Description:"Config file location"` LogFile string `long:"logfile" Description:"Log file location"` LogLevel string `long:"loglevel" Description:"Log level: debug, info, warning, error"` + + LND *backends.LND `group:"LND" namespace:"lnd"` } var cfg config +var backend backends.Backend +var backendName string + func initConfig() { + lndDir := getDefaultLndDir() + cfg = config{ ConfigFile: defaultConfigFile, LogFile: defaultLogFile, LogLevel: defaultLogLevel, + + LND: &backends.LND{ + RPCHost: defaultLndRPCHost, + CertFile: path.Join(lndDir, defaultLndCertFile), + MacaroonFile: path.Join(lndDir, defaultMacaroonFile), + }, } _, err := flags.Parse(&cfg) @@ -58,7 +79,30 @@ func initConfig() { } if errFile != nil { - log.Infof("Could not parse config file: %v", errFile) + log.Infof("Failed to parse config file: %v", errFile) } + // TODO: add more backend options like for example c-lighting + backend = cfg.LND + backendName = "LND" +} + +func getDefaultLndDir() (dir string) { + usr, err := user.Current() + + if err == nil { + switch runtime.GOOS { + case "windows": + dir = path.Join(usr.HomeDir, "AppData/Local/Lnd") + + case "darwin": + dir = path.Join(usr.HomeDir, "Library/Application Support/Lnd/tls.cert") + + default: + dir = path.Join(usr.HomeDir, ".lnd") + } + + } + + return dir } diff --git a/lightningtip.go b/lightningtip.go index d55030a..fb0e2c6 100644 --- a/lightningtip.go +++ b/lightningtip.go @@ -1,7 +1,21 @@ package main +import "fmt" + func main() { initLog() initConfig() + + err := backend.Connect() + + if err == nil { + log.Info("Successfully connected to " + backendName) + + invoice, err := backend.GetInvoice("Just a test", 1, 3600) + + log.Info("Got invoice " + invoice) + log.Info(fmt.Sprint(err)) + } + }