diff --git a/agorad.go b/agorad.go new file mode 100644 index 0000000..164ae7a --- /dev/null +++ b/agorad.go @@ -0,0 +1,214 @@ +package client + +import ( + "context" + "errors" + "fmt" + "io" + "net" + "net/http" + "os" + "path/filepath" + "strings" + "sync" + + proxy "github.com/grpc-ecosystem/grpc-gateway/runtime" + "github.com/lightninglabs/agora/client/auctioneer" + "github.com/lightninglabs/agora/client/clmrpc" + "github.com/lightninglabs/agora/client/trader" + "github.com/lightninglabs/loop/lndclient" + "github.com/lightningnetwork/lnd/build" + "google.golang.org/grpc" +) + +// Start runs agorad in daemon mode. It will listen for grpc connections, +// execute commands and pass back auction status information. +func Start(config *Config) error { + // Show the version and exit if the version flag was specified. + appName := filepath.Base(os.Args[0]) + appName = strings.TrimSuffix(appName, filepath.Ext(appName)) + if config.ShowVersion { + fmt.Println(appName, "version", Version()) + os.Exit(0) + } + + // Special show command to list supported subsystems and exit. + if config.DebugLevel == "show" { + fmt.Printf("Supported subsystems: %v\n", + logWriter.SupportedSubsystems()) + os.Exit(0) + } + + // Append the network type to the log directory so it is + // "namespaced" per network in the same fashion as the data directory. + config.LogDir = filepath.Join(config.LogDir, config.Network) + + // Initialize logging at the default logging level. + err := logWriter.InitLogRotator( + filepath.Join(config.LogDir, DefaultLogFilename), + config.MaxLogFileSize, config.MaxLogFiles, + ) + if err != nil { + return err + } + err = build.ParseAndSetDebugLevels(config.DebugLevel, logWriter) + if err != nil { + return err + } + + // Print the version before executing either primary directive. + log.Infof("Version: %v", Version()) + + lnd, err := getLnd(config.Network, config.Lnd) + if err != nil { + return err + } + defer lnd.Close() + + // If no auction server is specified, use the default addresses for + // mainnet and testnet. + if config.AuctionServer == "" && len(config.AuctioneerDialOpts) == 0 { + switch config.Network { + case "mainnet": + config.AuctionServer = MainnetServer + case "testnet": + config.AuctionServer = TestnetServer + default: + return errors.New("no auction server address specified") + } + } + + log.Infof("Auction server address: %v", config.AuctionServer) + + // Create an instance of the auctioneer client library. + auctioneerClient, cleanup, err := auctioneer.NewClient( + config.AuctionServer, config.Insecure, config.TLSPathAuctSrv, + lnd.WalletKit, config.AuctioneerDialOpts..., + ) + if err != nil { + return err + } + defer cleanup() + + // Instantiate the agorad gRPC server. + networkDir := filepath.Join(config.BaseDir, config.Network) + traderServer, err := trader.NewServer( + &lnd.LndServices, auctioneerClient, networkDir, + ) + if err != nil { + return err + } + + serverOpts := []grpc.ServerOption{} + grpcServer := grpc.NewServer(serverOpts...) + clmrpc.RegisterChannelAuctioneerClientServer(grpcServer, traderServer) + + // Next, start the gRPC server listening for HTTP/2 connections. + // If the provided grpcListener is not nil, it means agorad is being + // used as a library and the listener might not be a real network + // connection (but maybe a UNIX socket or bufconn). So we don't spin up + // a REST listener in that case. + log.Infof("Starting gRPC listener") + var ( + wg sync.WaitGroup + grpcListener = config.RPCListener + restListener net.Listener + restProxy *http.Server + ) + if grpcListener == nil { + grpcListener, err = net.Listen("tcp", config.RPCListen) + if err != nil { + return fmt.Errorf("RPC server unable to listen on %s", + config.RPCListen) + + } + defer closeOrLog(grpcListener) + + // We'll also create and start an accompanying proxy to serve + // clients through REST. + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + mux := proxy.NewServeMux() + proxyOpts := []grpc.DialOption{grpc.WithInsecure()} + err = clmrpc.RegisterChannelAuctioneerClientHandlerFromEndpoint( + ctx, mux, config.RPCListen, proxyOpts, + ) + if err != nil { + return err + } + + log.Infof("Starting REST proxy listener") + restListener, err = net.Listen("tcp", config.RESTListen) + if err != nil { + return fmt.Errorf("REST proxy unable to listen on %s", + config.RESTListen) + } + defer closeOrLog(restListener) + restProxy = &http.Server{Handler: mux} + wg.Add(1) + go func() { + defer wg.Done() + + err := restProxy.Serve(restListener) + if err != nil && err != http.ErrServerClosed { + log.Errorf("could not start rest listener: %v", + err) + } + }() + } + + // Start the trader server itself. + err = traderServer.Start() + if err != nil { + return err + } + + // Start the grpc server. + wg.Add(1) + go func() { + defer wg.Done() + + log.Infof("RPC server listening on %s", grpcListener.Addr()) + if restListener != nil { + log.Infof("REST proxy listening on %s", + restListener.Addr()) + } + + err = grpcServer.Serve(grpcListener) + if err != nil { + log.Error(err) + } + }() + + // Run until the user terminates agorad. + <-config.ShutdownChannel + log.Info("Received shutdown signal, stopping server") + grpcServer.GracefulStop() + if restProxy != nil { + err := restProxy.Shutdown(context.Background()) + if err != nil { + log.Errorf("error shutting down REST proxy: %v", err) + } + } + err = traderServer.Stop() + if err != nil { + log.Errorf("error shutting down server: %v", err) + } + + wg.Wait() + return nil +} + +// getLnd returns an instance of the lnd services proxy. +func getLnd(network string, cfg *LndConfig) (*lndclient.GrpcLndServices, error) { + return lndclient.NewLndServices( + cfg.Host, network, cfg.MacaroonDir, cfg.TLSPath, + ) +} + +func closeOrLog(c io.Closer) { + err := c.Close() + if err != nil { + log.Errorf("could not close: %v", err) + } +} diff --git a/auctioneer/client.go b/auctioneer/client.go index 07d617e..2f34346 100644 --- a/auctioneer/client.go +++ b/auctioneer/client.go @@ -22,10 +22,11 @@ type Client struct { // NewClient returns a new instance to initiate auctions with. func NewClient(serverAddress string, insecure bool, tlsPathServer string, - wallet lndclient.WalletKitClient) (*Client, func(), error) { + wallet lndclient.WalletKitClient, dialOpts ...grpc.DialOption) ( + *Client, func(), error) { serverConn, err := getAuctionServerConn( - serverAddress, insecure, tlsPathServer, + serverAddress, insecure, tlsPathServer, dialOpts..., ) if err != nil { return nil, nil, err @@ -42,11 +43,11 @@ func NewClient(serverAddress string, insecure bool, tlsPathServer string, } // getAuctionServerConn returns a connection to the auction server. -func getAuctionServerConn(address string, insecure bool, tlsPath string) ( - *grpc.ClientConn, error) { +func getAuctionServerConn(address string, insecure bool, tlsPath string, + dialOpts ...grpc.DialOption) (*grpc.ClientConn, error) { - // Create a dial options array. - opts := []grpc.DialOption{} + // Create a copy of the dial options array. + opts := dialOpts // There are three options to connect to a auction server, either // insecure, using a self-signed certificate or with a certificate diff --git a/cmd/agorad/daemon.go b/cmd/agorad/daemon.go deleted file mode 100644 index 1439b3f..0000000 --- a/cmd/agorad/daemon.go +++ /dev/null @@ -1,148 +0,0 @@ -package main - -import ( - "context" - "errors" - "fmt" - "io" - "net" - "net/http" - "sync" - - proxy "github.com/grpc-ecosystem/grpc-gateway/runtime" - "github.com/lightninglabs/agora/client/auctioneer" - "github.com/lightninglabs/agora/client/clmrpc" - "github.com/lightninglabs/agora/client/trader" - "github.com/lightningnetwork/lnd/signal" - "google.golang.org/grpc" -) - -// daemon runs agorad in daemon mode. It will listen for grpc connections, -// execute commands and pass back auction status information. -func daemon(config *config) error { - lnd, err := getLnd(config.Network, config.Lnd) - if err != nil { - return err - } - defer lnd.Close() - - // If no auction server is specified, use the default addresses for - // mainnet and testnet. - if config.AuctionServer == "" { - switch config.Network { - case "mainnet": - config.AuctionServer = mainnetServer - case "testnet": - config.AuctionServer = testnetServer - default: - return errors.New("no auction server address specified") - } - } - - log.Infof("Auction server address: %v", config.AuctionServer) - - // Create an instance of the auctioneer client library. - auctioneerClient, cleanup, err := auctioneer.NewClient( - config.AuctionServer, config.Insecure, config.TLSPathAuctSrv, - lnd.WalletKit, - ) - if err != nil { - return err - } - defer cleanup() - - // Instantiate the agorad gRPC server. - traderServer, err := trader.NewServer( - &lnd.LndServices, auctioneerClient, config.serverDir, - ) - if err != nil { - return err - } - - serverOpts := []grpc.ServerOption{} - grpcServer := grpc.NewServer(serverOpts...) - clmrpc.RegisterChannelAuctioneerClientServer(grpcServer, traderServer) - - // Next, start the gRPC server listening for HTTP/2 connections. - log.Infof("Starting gRPC listener") - grpcListener, err := net.Listen("tcp", config.RPCListen) - if err != nil { - return fmt.Errorf("RPC server unable to listen on %s", - config.RPCListen) - - } - defer closeOrLog(grpcListener) - - // We'll also create and start an accompanying proxy to serve clients - // through REST. - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - mux := proxy.NewServeMux() - proxyOpts := []grpc.DialOption{grpc.WithInsecure()} - err = clmrpc.RegisterChannelAuctioneerClientHandlerFromEndpoint( - ctx, mux, config.RPCListen, proxyOpts, - ) - if err != nil { - return err - } - - log.Infof("Starting REST proxy listener") - restListener, err := net.Listen("tcp", config.RESTListen) - if err != nil { - return fmt.Errorf("REST proxy unable to listen on %s", - config.RESTListen) - } - defer closeOrLog(restListener) - restProxy := &http.Server{Handler: mux} - go func() { - err := restProxy.Serve(restListener) - if err != nil && err != http.ErrServerClosed { - log.Errorf("could not start rest listener: %v", err) - } - }() - - // Start the trader server itself. - err = traderServer.Start() - if err != nil { - return err - } - - // Start the grpc server. - var wg sync.WaitGroup - wg.Add(1) - go func() { - defer wg.Done() - - log.Infof("RPC server listening on %s", grpcListener.Addr()) - log.Infof("REST proxy listening on %s", restListener.Addr()) - - err = grpcServer.Serve(grpcListener) - if err != nil { - log.Error(err) - } - }() - - // Run until the user terminates agorad. - signal.Intercept() - <-signal.ShutdownChannel() - log.Info("Received shutdown signal, stopping server") - grpcServer.GracefulStop() - err = restProxy.Shutdown(context.Background()) - if err != nil { - log.Errorf("error shutting down REST proxy: %v", err) - } - err = traderServer.Stop() - if err != nil { - log.Errorf("error shutting down server: %v", err) - } - - wg.Wait() - return nil -} - -func closeOrLog(c io.Closer) { - err := c.Close() - if err != nil { - log.Errorf("could not close: %v", err) - } -} diff --git a/cmd/agorad/main.go b/cmd/agorad/main.go index c39482d..8a57ec2 100644 --- a/cmd/agorad/main.go +++ b/cmd/agorad/main.go @@ -4,11 +4,10 @@ import ( "fmt" "os" "path/filepath" - "strings" "github.com/jessevdk/go-flags" "github.com/lightninglabs/agora/client" - "github.com/lightningnetwork/lnd/build" + "github.com/lightningnetwork/lnd/signal" ) var ( @@ -23,7 +22,7 @@ func main() { } func start() error { - config := defaultConfig + config := client.DefaultConfig // Parse command line flags. parser := flags.NewParser(&config, flags.Default) @@ -38,12 +37,12 @@ func start() error { } // Parse ini file. - config.serverDir = filepath.Join(agoraDirBase, config.Network) - if err := os.MkdirAll(config.serverDir, os.ModePerm); err != nil { + networkDir := filepath.Join(config.BaseDir, config.Network) + if err := os.MkdirAll(networkDir, os.ModePerm); err != nil { return err } - configFile := filepath.Join(config.serverDir, defaultConfigFilename) + configFile := filepath.Join(networkDir, 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 @@ -60,44 +59,11 @@ func start() error { return err } - // Show the version and exit if the version flag was specified. - appName := filepath.Base(os.Args[0]) - appName = strings.TrimSuffix(appName, filepath.Ext(appName)) - if config.ShowVersion { - fmt.Println(appName, "version", client.Version()) - os.Exit(0) - } - - // Special show command to list supported subsystems and exit. - if config.DebugLevel == "show" { - fmt.Printf("Supported subsystems: %v\n", - logWriter.SupportedSubsystems()) - os.Exit(0) - } - - // Append the network type to the log directory so it is - // "namespaced" per network in the same fashion as the data directory. - config.LogDir = filepath.Join(config.LogDir, config.Network) - - // Initialize logging at the default logging level. - err = logWriter.InitLogRotator( - filepath.Join(config.LogDir, defaultLogFilename), - config.MaxLogFileSize, config.MaxLogFiles, - ) - if err != nil { - return err - } - err = build.ParseAndSetDebugLevels(config.DebugLevel, logWriter) - if err != nil { - return err - } - - // Print the version before executing either primary directive. - log.Infof("Version: %v", client.Version()) - // Execute command. if parser.Active == nil { - return daemon(&config) + signal.Intercept() + config.ShutdownChannel = signal.ShutdownChannel() + return client.Start(&config) } return fmt.Errorf("unimplemented command %v", parser.Active.Name) diff --git a/cmd/agorad/utils.go b/cmd/agorad/utils.go deleted file mode 100644 index 8917a5b..0000000 --- a/cmd/agorad/utils.go +++ /dev/null @@ -1,12 +0,0 @@ -package main - -import ( - "github.com/lightninglabs/loop/lndclient" -) - -// getLnd returns an instance of the lnd services proxy. -func getLnd(network string, cfg *lndConfig) (*lndclient.GrpcLndServices, error) { - return lndclient.NewLndServices( - cfg.Host, network, cfg.MacaroonDir, cfg.TLSPath, - ) -} diff --git a/cmd/agorad/config.go b/config.go similarity index 56% rename from cmd/agorad/config.go rename to config.go index 61778bc..95093a7 100644 --- a/cmd/agorad/config.go +++ b/config.go @@ -1,30 +1,39 @@ -package main +package client import ( + "net" "path/filepath" "github.com/btcsuite/btcutil" + "google.golang.org/grpc" ) var ( - agoraDirBase = btcutil.AppDataDir("agora", false) + // DefaultBaseDir is the default root data directory where agora will + // store all its data. On UNIX like systems this will resolve to + // ~/.agora. Below this directory the logs and network directory will be + // created. + DefaultBaseDir = btcutil.AppDataDir("agora", false) - defaultLogLevel = "info" - defaultLogDirname = "logs" - defaultLogFilename = "agorad.log" - defaultLogDir = filepath.Join(agoraDirBase, defaultLogDirname) + // DefaultLogFilename is the default name that is given to the agora log + // file. + DefaultLogFilename = "agorad.log" + + defaultLogLevel = "info" + defaultLogDirname = "logs" + defaultLogDir = filepath.Join(DefaultBaseDir, defaultLogDirname) defaultMaxLogFiles = 3 defaultMaxLogFileSize = 10 ) -type lndConfig struct { +type LndConfig struct { Host string `long:"host" description:"lnd instance rpc address"` MacaroonDir string `long:"macaroondir" description:"Path to the directory containing all the required lnd macaroons"` TLSPath string `long:"tlspath" description:"Path to lnd tls certificate"` } -type config struct { +type Config struct { ShowVersion bool `short:"V" long:"version" description:"Display version information and exit"` Insecure bool `long:"insecure" description:"disable tls"` Network string `long:"network" description:"network to run on" choice:"regtest" choice:"testnet" choice:"mainnet" choice:"simnet"` @@ -32,6 +41,7 @@ type config struct { TLSPathAuctSrv string `long:"tlspathauctserver" description:"Path to auction server tls certificate"` RPCListen string `long:"rpclisten" description:"Address to listen on for gRPC clients"` RESTListen string `long:"restlisten" description:"Address to listen on for REST clients"` + BaseDir string `long:"basedir" description:"The base directory where agora stores all its data"` LogDir string `long:"logdir" description:"Directory to log output."` MaxLogFiles int `long:"maxlogfiles" description:"Maximum logfiles to keep (0 for no rotation)"` @@ -39,26 +49,39 @@ type config struct { DebugLevel string `short:"d" long:"debuglevel" description:"Logging level for all subsystems {trace, debug, info, warn, error, critical} -- You may also specify =,=,... to set the log level for individual subsystems -- Use show to list available subsystems"` - Lnd *lndConfig `group:"lnd" namespace:"lnd"` + Lnd *LndConfig `group:"lnd" namespace:"lnd"` - serverDir string + // RPCListener is a network listener that can be set if agorad should be + // used as a library and listen on the given listener instead of what is + // configured in the --rpclisten parameter. Setting this will also + // disable REST. + RPCListener net.Listener + + // AuctioneerDialOpts is a list of dial options that should be used when + // dialing the auctioneer server. + AuctioneerDialOpts []grpc.DialOption + + // ShutdownChannel is the channel that must be provided where agorad + // listens for a shutdown signal. + ShutdownChannel <-chan struct{} } const ( - mainnetServer = "auction.lightning.today:12009" - testnetServer = "test.auction.lightning.today:12009" + MainnetServer = "auction.lightning.today:12009" + TestnetServer = "test.auction.lightning.today:12009" ) -var defaultConfig = config{ +var DefaultConfig = Config{ Network: "mainnet", RPCListen: "localhost:12010", RESTListen: "localhost:8281", Insecure: false, + BaseDir: DefaultBaseDir, LogDir: defaultLogDir, MaxLogFiles: defaultMaxLogFiles, MaxLogFileSize: defaultMaxLogFileSize, DebugLevel: defaultLogLevel, - Lnd: &lndConfig{ + Lnd: &LndConfig{ Host: "localhost:10009", }, } diff --git a/cmd/agorad/log.go b/log.go similarity index 99% rename from cmd/agorad/log.go rename to log.go index a19fafa..5000637 100644 --- a/cmd/agorad/log.go +++ b/log.go @@ -1,6 +1,6 @@ // As this file is very similar in every package, ignore the linter here. // nolint:dupl -package main +package client import ( "github.com/btcsuite/btclog"