diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..9852b33 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,25 @@ +language: go +cache: + directories: + - $GOCACHE + - $GOPATH/pkg/mod + - $GOPATH/src/github.com/btcsuite + - $GOPATH/src/github.com/golang + - $GOPATH/src/gopkg.in/alecthomas + +go: + - "1.11.x" + +env: + global: + - GOCACHE=$HOME/.go-build + +sudo: required + +script: + - export GO111MODULE=on + - make unit + +after_script: + - echo "Uploading to termbin.com..." && find *.log | xargs -I{} sh -c "cat {} | nc termbin.com 9999 | xargs -r0 printf '{} uploaded to %s'" + - echo "Uploading to file.io..." && tar -zcvO *.log | curl -s -F 'file=@-;filename=logs.tar.gz' https://file.io | xargs -r0 printf 'logs.tar.gz uploaded to %s\n' diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..acca9e7 --- /dev/null +++ b/Makefile @@ -0,0 +1,15 @@ +PKG := github.com/lightninglabs/loop + +GOTEST := GO111MODULE=on go test -v + +GOLIST := go list $(PKG)/... | grep -v '/vendor/' + +XARGS := xargs -L 1 + +TEST_FLAGS = -test.timeout=20m + +UNIT := $(GOLIST) | $(XARGS) env $(GOTEST) $(TEST_FLAGS) + +unit: + @$(call print, "Running unit tests.") + $(UNIT) diff --git a/cmd/loop/commands.go b/cmd/loop/loopout.go similarity index 56% rename from cmd/loop/commands.go rename to cmd/loop/loopout.go index 6c216a1..6767a84 100644 --- a/cmd/loop/commands.go +++ b/cmd/loop/loopout.go @@ -4,9 +4,7 @@ import ( "context" "fmt" - "github.com/btcsuite/btcutil" "github.com/lightninglabs/loop/looprpc" - "github.com/lightninglabs/loop/swap" "github.com/urfave/cli" ) @@ -97,81 +95,3 @@ func loopOut(ctx *cli.Context) error { return nil } - -var termsCommand = cli.Command{ - Name: "terms", - Usage: "show current server swap terms", - Action: terms, -} - -func terms(ctx *cli.Context) error { - client, cleanup, err := getClient(ctx) - if err != nil { - return err - } - defer cleanup() - - terms, err := client.GetLoopOutTerms( - context.Background(), &looprpc.TermsRequest{}, - ) - if err != nil { - return err - } - - fmt.Printf("Amount: %d - %d\n", - btcutil.Amount(terms.MinSwapAmount), - btcutil.Amount(terms.MaxSwapAmount), - ) - if err != nil { - return err - } - - printTerms := func(terms *looprpc.TermsResponse) { - fmt.Printf("Amount: %d - %d\n", - btcutil.Amount(terms.MinSwapAmount), - btcutil.Amount(terms.MaxSwapAmount), - ) - fmt.Printf("Fee: %d + %.4f %% (%d prepaid)\n", - btcutil.Amount(terms.SwapFeeBase), - swap.FeeRateAsPercentage(terms.SwapFeeRate), - btcutil.Amount(terms.PrepayAmt), - ) - - fmt.Printf("Cltv delta: %v blocks\n", terms.CltvDelta) - } - - fmt.Println("Loop Out") - fmt.Println("--------") - printTerms(terms) - - return nil -} - -var monitorCommand = cli.Command{ - Name: "monitor", - Usage: "monitor progress of any active swaps", - Description: "Allows the user to monitor progress of any active swaps", - Action: monitor, -} - -func monitor(ctx *cli.Context) error { - client, cleanup, err := getClient(ctx) - if err != nil { - return err - } - defer cleanup() - - stream, err := client.Monitor( - context.Background(), &looprpc.MonitorRequest{}) - if err != nil { - return err - } - - for { - swap, err := stream.Recv() - if err != nil { - return fmt.Errorf("recv: %v", err) - } - logSwap(swap) - } -} diff --git a/cmd/loop/main.go b/cmd/loop/main.go index 73a4883..9e44fb0 100644 --- a/cmd/loop/main.go +++ b/cmd/loop/main.go @@ -17,8 +17,6 @@ import ( ) var ( - loopdAddress = "localhost:11010" - // Define route independent max routing fees. We have currently no way // to get a reliable estimate of the routing fees. Best we can do is // the minimum routing fees, which is not very indicative. @@ -38,6 +36,13 @@ func main() { app.Version = "0.0.1" app.Name = "loop" app.Usage = "control plane for your loopd" + app.Flags = []cli.Flag{ + cli.StringFlag{ + Name: "loopd", + Value: "localhost:11010", + Usage: "loopd daemon address host:port", + }, + } app.Commands = []cli.Command{ loopOutCommand, termsCommand, monitorCommand, } @@ -49,6 +54,7 @@ func main() { } func getClient(ctx *cli.Context) (looprpc.SwapClientClient, func(), error) { + loopdAddress := ctx.GlobalString("loopd") conn, err := getClientConn(loopdAddress) if err != nil { return nil, nil, err diff --git a/cmd/loop/monitor.go b/cmd/loop/monitor.go new file mode 100644 index 0000000..8f51fcf --- /dev/null +++ b/cmd/loop/monitor.go @@ -0,0 +1,38 @@ +package main + +import ( + "context" + "fmt" + + "github.com/lightninglabs/loop/looprpc" + "github.com/urfave/cli" +) + +var monitorCommand = cli.Command{ + Name: "monitor", + Usage: "monitor progress of any active swaps", + Description: "Allows the user to monitor progress of any active swaps", + Action: monitor, +} + +func monitor(ctx *cli.Context) error { + client, cleanup, err := getClient(ctx) + if err != nil { + return err + } + defer cleanup() + + stream, err := client.Monitor( + context.Background(), &looprpc.MonitorRequest{}) + if err != nil { + return err + } + + for { + swap, err := stream.Recv() + if err != nil { + return fmt.Errorf("recv: %v", err) + } + logSwap(swap) + } +} diff --git a/cmd/loop/terms.go b/cmd/loop/terms.go new file mode 100644 index 0000000..960779b --- /dev/null +++ b/cmd/loop/terms.go @@ -0,0 +1,61 @@ +package main + +import ( + "context" + "fmt" + + "github.com/btcsuite/btcutil" + "github.com/lightninglabs/loop/swap" + + "github.com/lightninglabs/loop/looprpc" + "github.com/urfave/cli" +) + +var termsCommand = cli.Command{ + Name: "terms", + Usage: "show current server swap terms", + Action: terms, +} + +func terms(ctx *cli.Context) error { + client, cleanup, err := getClient(ctx) + if err != nil { + return err + } + defer cleanup() + + terms, err := client.GetLoopOutTerms( + context.Background(), &looprpc.TermsRequest{}, + ) + if err != nil { + return err + } + + fmt.Printf("Amount: %d - %d\n", + btcutil.Amount(terms.MinSwapAmount), + btcutil.Amount(terms.MaxSwapAmount), + ) + if err != nil { + return err + } + + printTerms := func(terms *looprpc.TermsResponse) { + fmt.Printf("Amount: %d - %d\n", + btcutil.Amount(terms.MinSwapAmount), + btcutil.Amount(terms.MaxSwapAmount), + ) + fmt.Printf("Fee: %d + %.4f %% (%d prepaid)\n", + btcutil.Amount(terms.SwapFeeBase), + swap.FeeRateAsPercentage(terms.SwapFeeRate), + btcutil.Amount(terms.PrepayAmt), + ) + + fmt.Printf("Cltv delta: %v blocks\n", terms.CltvDelta) + } + + fmt.Println("Loop Out") + fmt.Println("--------") + printTerms(terms) + + return nil +} diff --git a/cmd/loopd/config.go b/cmd/loopd/config.go new file mode 100644 index 0000000..04e4315 --- /dev/null +++ b/cmd/loopd/config.go @@ -0,0 +1,30 @@ +package main + +type lndConfig struct { + Host string `long:"host" description:"lnd instance rpc address"` + MacaroonPath string `long:"macaroonpath" description:"Path to lnd macaroon"` + TLSPath string `long:"tlspath" description:"Path to lnd tls certificate"` +} + +type viewParameters struct{} + +type config struct { + Insecure bool `long:"insecure" description:"disable tls"` + Network string `long:"network" description:"network to run on" choice:"regtest" choice:"testnet" choice:"mainnet" choice:"simnet"` + SwapServer string `long:"swapserver" description:"swap server address host:port"` + Listen string `long:"listen" description:"address to listen on for rpc lcients"` + + Lnd *lndConfig `group:"lnd" namespace:"lnd"` + + View viewParameters `command:"view" alias:"v" description:"View all swaps in the database. This command can only be executed when loopd is not running."` +} + +var defaultConfig = config{ + Network: "mainnet", + SwapServer: "swap.lightning.today:11009", + Listen: "localhost:11010", + Insecure: false, + Lnd: &lndConfig{ + Host: "localhost:10009", + }, +} diff --git a/cmd/loopd/daemon.go b/cmd/loopd/daemon.go index 8acca5c..9018763 100644 --- a/cmd/loopd/daemon.go +++ b/cmd/loopd/daemon.go @@ -12,20 +12,21 @@ import ( "github.com/lightninglabs/loop" "github.com/lightninglabs/loop/looprpc" - "github.com/urfave/cli" "google.golang.org/grpc" ) -// daemon runs swapd in daemon mode. It will listen for grpc connections, +// daemon runs loopd in daemon mode. It will listen for grpc connections, // execute commands and pass back swap status information. -func daemon(ctx *cli.Context) error { - lnd, err := getLnd(ctx) +func daemon(config *config) error { + lnd, err := getLnd(config.Network, config.Lnd) if err != nil { return err } defer lnd.Close() - swapClient, cleanup, err := getClient(ctx, &lnd.LndServices) + swapClient, cleanup, err := getClient( + config.Network, config.SwapServer, config.Insecure, &lnd.LndServices, + ) if err != nil { return err } @@ -48,7 +49,7 @@ func daemon(ctx *cli.Context) error { } } - // Instantiate the swapd gRPC server. + // Instantiate the loopd gRPC server. server := swapClientServer{ impl: swapClient, lnd: &lnd.LndServices, @@ -60,10 +61,10 @@ func daemon(ctx *cli.Context) error { // Next, Start the gRPC server listening for HTTP/2 connections. logger.Infof("Starting RPC listener") - lis, err := net.Listen("tcp", defaultListenAddr) + lis, err := net.Listen("tcp", config.Listen) if err != nil { return fmt.Errorf("RPC server unable to listen on %s", - defaultListenAddr) + config.Listen) } defer lis.Close() @@ -134,7 +135,7 @@ func daemon(ctx *cli.Context) error { interruptChannel := make(chan os.Signal, 1) signal.Notify(interruptChannel, os.Interrupt) - // Run until the users terminates swapd or an error occurred. + // Run until the users terminates loopd or an error occurred. select { case <-interruptChannel: logger.Infof("Received SIGINT (Ctrl+C).") diff --git a/cmd/loopd/log.go b/cmd/loopd/log.go index 8feea95..5ec7eef 100644 --- a/cmd/loopd/log.go +++ b/cmd/loopd/log.go @@ -11,7 +11,7 @@ import ( // it. var ( backendLog = btclog.NewBackend(logWriter{}) - logger = backendLog.Logger("SWAPD") + logger = backendLog.Logger("LOOPD") ) // logWriter implements an io.Writer that outputs to both standard output and diff --git a/cmd/loopd/main.go b/cmd/loopd/main.go index 49f0e3c..4df2e03 100644 --- a/cmd/loopd/main.go +++ b/cmd/loopd/main.go @@ -3,22 +3,23 @@ package main import ( "fmt" "os" + "path/filepath" "sync" + flags "github.com/jessevdk/go-flags" + "github.com/btcsuite/btcutil" "github.com/lightninglabs/loop" "github.com/lightningnetwork/lnd/lntypes" - "github.com/urfave/cli" ) const ( - defaultListenPort = 11010 defaultConfTarget = int32(2) ) var ( - defaultListenAddr = fmt.Sprintf("localhost:%d", defaultListenPort) - defaultSwapletDir = btcutil.AppDataDir("swaplet", false) + loopDirBase = btcutil.AppDataDir("loop", false) + defaultConfigFilename = "loopd.conf" swaps = make(map[lntypes.Hash]loop.SwapInfo) subscribers = make(map[int]chan<- interface{}) @@ -27,47 +28,58 @@ var ( ) func main() { - app := cli.NewApp() - - app.Flags = []cli.Flag{ - cli.StringFlag{ - Name: "network", - Value: "mainnet", - Usage: "network to run on (regtest, testnet, mainnet)", - }, - cli.StringFlag{ - Name: "lnd", - Value: "localhost:10009", - Usage: "lnd instance rpc address host:port", - }, - cli.StringFlag{ - Name: "swapserver", - Value: "swap.lightning.today:11009", - Usage: "swap server address host:port", - }, - cli.StringFlag{ - Name: "macaroonpath", - Usage: "path to lnd macaroon", - }, - cli.StringFlag{ - Name: "tlspath", - Usage: "path to lnd tls certificate", - }, - cli.BoolFlag{ - Name: "insecure", - Usage: "disable tls", - }, + err := start() + if err != nil { + fmt.Println(err) + } +} + +func start() error { + config := defaultConfig + + // Parse command line flags. + parser := flags.NewParser(&config, flags.Default) + parser.SubcommandsOptional = true + + _, err := parser.Parse() + if e, ok := err.(*flags.Error); ok && e.Type == flags.ErrHelp { + return nil } - app.Name = "loopd" - app.Version = "0.0.1" - app.Usage = "Lightning Loop Client Daemon" - app.Commands = []cli.Command{ - viewCommand, + if err != nil { + return err } - app.Action = daemon - err := app.Run(os.Args) + // Parse ini file. + loopDir := filepath.Join(loopDirBase, config.Network) + if err := os.MkdirAll(loopDir, os.ModePerm); err != nil { + return err + } + + configFile := filepath.Join(loopDir, defaultConfigFilename) + if err := flags.IniParse(configFile, &config); err != nil { + // If it's a parsing related error, then we'll return + // immediately, otherwise we can proceed as possibly the config + // file doesn't exist which is OK. + if _, ok := err.(*flags.IniError); ok { + return err + } + } + + // Parse command line flags again to restore flags overwritten by ini + // parse. + _, err = parser.Parse() if err != nil { - fmt.Println(err) + return err } + + // Execute command. + if parser.Active == nil { + return daemon(&config) + } + + if parser.Active.Name == "view" { + return view(&config) + } + + return fmt.Errorf("unimplemented command %v", parser.Active.Name) } diff --git a/cmd/loopd/utils.go b/cmd/loopd/utils.go index 7e7f2f9..787cf23 100644 --- a/cmd/loopd/utils.go +++ b/cmd/loopd/utils.go @@ -6,33 +6,26 @@ import ( "github.com/lightninglabs/loop" "github.com/lightninglabs/loop/lndclient" - "github.com/urfave/cli" ) // getLnd returns an instance of the lnd services proxy. -func getLnd(ctx *cli.Context) (*lndclient.GrpcLndServices, error) { - network := ctx.GlobalString("network") - - return lndclient.NewLndServices(ctx.GlobalString("lnd"), - "client", network, ctx.GlobalString("macaroonpath"), - ctx.GlobalString("tlspath"), +func getLnd(network string, cfg *lndConfig) (*lndclient.GrpcLndServices, error) { + return lndclient.NewLndServices( + cfg.Host, "client", network, cfg.MacaroonPath, cfg.TLSPath, ) } // getClient returns an instance of the swap client. -func getClient(ctx *cli.Context, +func getClient(network, swapServer string, insecure bool, lnd *lndclient.LndServices) (*loop.Client, func(), error) { - network := ctx.GlobalString("network") - storeDir, err := getStoreDir(network) if err != nil { return nil, nil, err } swapClient, cleanUp, err := loop.NewClient( - storeDir, ctx.GlobalString("swapserver"), - ctx.GlobalBool("insecure"), lnd, + storeDir, swapServer, insecure, lnd, ) if err != nil { return nil, nil, err @@ -42,7 +35,7 @@ func getClient(ctx *cli.Context, } func getStoreDir(network string) (string, error) { - dir := filepath.Join(defaultSwapletDir, network) + dir := filepath.Join(loopDirBase, network) if err := os.MkdirAll(dir, os.ModePerm); err != nil { return "", err } diff --git a/cmd/loopd/view.go b/cmd/loopd/view.go index b7373dd..40101c1 100644 --- a/cmd/loopd/view.go +++ b/cmd/loopd/view.go @@ -5,34 +5,24 @@ import ( "strconv" "github.com/lightninglabs/loop/swap" - "github.com/urfave/cli" ) -var viewCommand = cli.Command{ - Name: "view", - Usage: `view all swaps in the database. This command can only be - executed when swapd is not running.`, - Description: ` - Show all pending and completed swaps.`, - Action: view, -} - // view prints all swaps currently in the database. -func view(ctx *cli.Context) error { - network := ctx.GlobalString("network") - - chainParams, err := swap.ChainParamsFromNetwork(network) +func view(config *config) error { + chainParams, err := swap.ChainParamsFromNetwork(config.Network) if err != nil { return err } - lnd, err := getLnd(ctx) + lnd, err := getLnd(config.Network, config.Lnd) if err != nil { return err } defer lnd.Close() - swapClient, cleanup, err := getClient(ctx, &lnd.LndServices) + swapClient, cleanup, err := getClient( + config.Network, config.SwapServer, config.Insecure, &lnd.LndServices, + ) if err != nil { return err } @@ -43,6 +33,10 @@ func view(ctx *cli.Context) error { return err } + if len(swaps) == 0 { + fmt.Printf("No swaps\n") + } + for _, s := range swaps { htlc, err := swap.NewHtlc( s.Contract.CltvExpiry, diff --git a/lndclient/lnd_services.go b/lndclient/lnd_services.go index ab40949..3d3bb54 100644 --- a/lndclient/lnd_services.go +++ b/lndclient/lnd_services.go @@ -45,7 +45,7 @@ func NewLndServices(lndAddress string, application string, *GrpcLndServices, error) { // Setup connection with lnd - logger.Infof("Creating lnd connection") + logger.Infof("Creating lnd connection to %v", lndAddress) conn, err := getClientConn(lndAddress, network, macPath, tlsPath) if err != nil { return nil, err