From 4536e992758ab7ea825d61f1c182f81c0d3137de Mon Sep 17 00:00:00 2001 From: Ivan Daniluk Date: Fri, 3 Nov 2017 23:07:13 +0100 Subject: [PATCH] Improve statusd CLI usage (#441) This PR refactors CLI API, removes obsolete commands and splits status code into smaller pieces: * get rid of subcommands API (no ./status ) * get rid of custom cli app package * use stdlib flag package for handling command line flags * move cross-compilation / mobile related code to lib/ package * move wnode command into separate binary (cmd/node-status, name is subject to discuss) * remove faucet command as obsolete * update/add docs/READMES/wikis for new command line flags It makes statusd code much simpler and smaller, separates concerns (lib, wnode and statusd are different things). --- .gitignore | 6 + Makefile | 27 +-- cmd/statusd/faucetcmd.go | 66 ------- cmd/statusd/lescmd.go | 68 ------- cmd/statusd/main.go | 244 +++++++++++------------- cmd/statusd/misccmd.go | 41 ---- cmd/statusd/wnodecmd.go | 269 --------------------------- cmd/wnode-status/config.go | 113 +++++++++++ cmd/wnode-status/main.go | 84 +++++++++ cmd/wnode-status/test_accounts.go | 69 +++++++ {cmd/statusd => lib}/library.go | 0 {cmd/statusd => lib}/library_test.go | 0 lib/main.go | 13 ++ {cmd/statusd => lib}/utils.go | 0 static/tests/whisper.js | 6 +- 15 files changed, 409 insertions(+), 597 deletions(-) delete mode 100644 cmd/statusd/faucetcmd.go delete mode 100644 cmd/statusd/lescmd.go delete mode 100644 cmd/statusd/misccmd.go delete mode 100644 cmd/statusd/wnodecmd.go create mode 100644 cmd/wnode-status/config.go create mode 100644 cmd/wnode-status/main.go create mode 100644 cmd/wnode-status/test_accounts.go rename {cmd/statusd => lib}/library.go (100%) rename {cmd/statusd => lib}/library_test.go (100%) create mode 100644 lib/main.go rename {cmd/statusd => lib}/utils.go (100%) diff --git a/.gitignore b/.gitignore index be3429164..a9e3d39c2 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,12 @@ # git config --global core.excludesfile ~/.gitignore_global /statusd-data +/cmd/statusd/statusd-data +/cmd/statusd/statusd + +/wnode-status-data +/cmd/wnode-status/wnode-status-data +/cmd/wnode-status/wnode-status /tmp */**/*un~ diff --git a/Makefile b/Makefile index 2bc3be3e0..b6c09e86c 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,9 @@ .PHONY: statusgo all test xgo clean help .PHONY: statusgo-android statusgo-ios +help: ##@other Show this help + @perl -e '$(HELP_FUN)' $(MAKEFILE_LIST) + include ./static/tools/mk/lint.mk GOBIN = build/bin @@ -28,32 +31,32 @@ HELP_FUN = \ print "\n"; \ } -help: ##@other Show this help - @perl -e '$(HELP_FUN)' $(MAKEFILE_LIST) - # Main targets UNIT_TEST_PACKAGES := $(shell go list ./... | grep -v /vendor | grep -v /e2e | grep -v /cmd) statusgo: ##@build Build status-go as statusd server build/env.sh go build -i -o $(GOBIN)/statusd -v $(shell build/testnet-flags.sh) ./cmd/statusd - @echo "\nCompilation done.\nRun \"build/bin/statusd help\" to view available commands." + @echo "\nCompilation done.\nRun \"build/bin/statusd -h\" to view available commands." + +wnode-status: ##@build Build wnode-status (Whisper 5 debug tool) + build/env.sh go build -i -o $(GOBIN)/wnode-status -v $(shell build/testnet-flags.sh) ./cmd/wnode-status statusgo-cross: statusgo-android statusgo-ios @echo "Full cross compilation done." @ls -ld $(GOBIN)/statusgo-* statusgo-android: xgo ##@cross-compile Build status-go for Android - build/env.sh $(GOBIN)/xgo --image farazdagi/xgo --go=$(GO) -out statusgo --dest=$(GOBIN) --targets=android-16/aar -v $(shell build/testnet-flags.sh) ./cmd/statusd + build/env.sh $(GOBIN)/xgo --image farazdagi/xgo --go=$(GO) -out statusgo --dest=$(GOBIN) --targets=android-16/aar -v $(shell build/testnet-flags.sh) ./lib @echo "Android cross compilation done." statusgo-ios: xgo ##@cross-compile Build status-go for iOS - build/env.sh $(GOBIN)/xgo --image farazdagi/xgo --go=$(GO) -out statusgo --dest=$(GOBIN) --targets=ios-9.3/framework -v $(shell build/testnet-flags.sh) ./cmd/statusd + build/env.sh $(GOBIN)/xgo --image farazdagi/xgo --go=$(GO) -out statusgo --dest=$(GOBIN) --targets=ios-9.3/framework -v $(shell build/testnet-flags.sh) ./lib @echo "iOS framework cross compilation done." statusgo-ios-simulator: xgo ##@cross-compile Build status-go for iOS Simulator @build/env.sh docker pull farazdagi/xgo-ios-simulator - build/env.sh $(GOBIN)/xgo --image farazdagi/xgo-ios-simulator --go=$(GO) -out statusgo --dest=$(GOBIN) --targets=ios-9.3/framework -v $(shell build/testnet-flags.sh) ./cmd/statusd + build/env.sh $(GOBIN)/xgo --image farazdagi/xgo-ios-simulator --go=$(GO) -out statusgo --dest=$(GOBIN) --targets=ios-9.3/framework -v $(shell build/testnet-flags.sh) ./lib @echo "iOS framework cross compilation done." xgo: @@ -66,15 +69,15 @@ statusgo-mainnet: @echo "Run \"build/bin/statusgo\" to view available commands" statusgo-android-mainnet: xgo - build/env.sh $(GOBIN)/xgo --image farazdagi/xgo --go=$(GO) -out statusgo --dest=$(GOBIN) --targets=android-16/aar -v $(shell build/mainnet-flags.sh) ./cmd/statusd + build/env.sh $(GOBIN)/xgo --image farazdagi/xgo --go=$(GO) -out statusgo --dest=$(GOBIN) --targets=android-16/aar -v $(shell build/mainnet-flags.sh) ./lib @echo "Android cross compilation done (mainnet)." statusgo-ios-mainnet: xgo - build/env.sh $(GOBIN)/xgo --image farazdagi/xgo --go=$(GO) -out statusgo --dest=$(GOBIN) --targets=ios-9.3/framework -v $(shell build/mainnet-flags.sh) ./cmd/statusd + build/env.sh $(GOBIN)/xgo --image farazdagi/xgo --go=$(GO) -out statusgo --dest=$(GOBIN) --targets=ios-9.3/framework -v $(shell build/mainnet-flags.sh) ./lib @echo "iOS framework cross compilation done (mainnet)." statusgo-ios-simulator-mainnet: xgo - build/env.sh $(GOBIN)/xgo --image farazdagi/xgo-ios-simulator --go=$(GO) -out statusgo --dest=$(GOBIN) --targets=ios-9.3/framework -v $(shell build/mainnet-flags.sh) ./cmd/statusd + build/env.sh $(GOBIN)/xgo --image farazdagi/xgo-ios-simulator --go=$(GO) -out statusgo --dest=$(GOBIN) --targets=ios-9.3/framework -v $(shell build/mainnet-flags.sh) ./lib @echo "iOS framework cross compilation done (mainnet)." generate: ##@other Regenerate assets and other auto-generated stuff @@ -109,10 +112,10 @@ test-e2e: ##@tests Run e2e tests build/env.sh go test -timeout 20m ./e2e/rpc/... -network=$(networkid) build/env.sh go test -timeout 20m ./e2e/whisper/... -network=$(networkid) build/env.sh go test -timeout 10m ./e2e/transactions/... -network=$(networkid) - build/env.sh go test -timeout 40m ./cmd/statusd -network=$(networkid) + build/env.sh go test -timeout 40m ./lib -network=$(networkid) ci: lint mock-install mock test-unit test-e2e ##@tests Run all linters and tests at once clean: ##@other Cleanup rm -fr build/bin/* - rm coverage.out coverage-all.out coverage.html \ No newline at end of file + rm coverage.out coverage-all.out coverage.html diff --git a/cmd/statusd/faucetcmd.go b/cmd/statusd/faucetcmd.go deleted file mode 100644 index 9d0474b16..000000000 --- a/cmd/statusd/faucetcmd.go +++ /dev/null @@ -1,66 +0,0 @@ -package main - -import ( - "fmt" - - "github.com/status-im/status-go/geth/params" - "gopkg.in/urfave/cli.v1" -) - -var ( - faucetCommand = cli.Command{ - Action: faucetCommandHandler, - Name: "faucet", - Usage: "Starts faucet node (light node used by faucet service to request Ether)", - Flags: []cli.Flag{ - HTTPPortFlag, - HTTPEnabledFlag, - }, - } -) - -// faucetCommandHandler handles `statusd faucet` command -func faucetCommandHandler(ctx *cli.Context) error { - config, err := parseFaucetCommandConfig(ctx) - if err != nil { - return fmt.Errorf("can not parse config: %v", err) - } - - fmt.Println("Starting Status Faucet node..") - if err = statusAPI.StartNode(config); err != nil { - return err - } - - // wait till node has been stopped - node, err := statusAPI.NodeManager().Node() - if err != nil { - return nil - } - node.Wait() - - return nil -} - -// parseFaucetCommandConfig parses incoming CLI options and returns node configuration object -func parseFaucetCommandConfig(ctx *cli.Context) (*params.NodeConfig, error) { - nodeConfig, err := makeNodeConfig(ctx) - if err != nil { - return nil, err - } - - // select sub-protocols - nodeConfig.LightEthConfig.Enabled = true - nodeConfig.WhisperConfig.Enabled = false - nodeConfig.SwarmConfig.Enabled = false - - // RPC configuration - nodeConfig.APIModules = "eth" - nodeConfig.HTTPHost = "0.0.0.0" // allow to connect from anywhere - nodeConfig.HTTPPort = ctx.Int(HTTPPortFlag.Name) - nodeConfig.RPCEnabled = ctx.Bool(HTTPEnabledFlag.Name) - - // extra options - nodeConfig.BootClusterConfig.Enabled = true - - return nodeConfig, nil -} diff --git a/cmd/statusd/lescmd.go b/cmd/statusd/lescmd.go deleted file mode 100644 index 0d3a1df45..000000000 --- a/cmd/statusd/lescmd.go +++ /dev/null @@ -1,68 +0,0 @@ -package main - -import ( - "fmt" - - "github.com/status-im/status-go/geth/params" - "gopkg.in/urfave/cli.v1" -) - -var ( - lesCommand = cli.Command{ - Action: lesCommandHandler, - Name: "les", - Usage: "Starts Light Ethereum node", - Flags: []cli.Flag{ - WhisperEnabledFlag, - SwarmEnabledFlag, - HTTPEnabledFlag, - HTTPPortFlag, - IPCEnabledFlag, - }, - } -) - -// lesCommandHandler handles `statusd les` command -func lesCommandHandler(ctx *cli.Context) error { - config, err := parseLESCommandConfig(ctx) - if err != nil { - return fmt.Errorf("can not parse config: %v", err) - } - - fmt.Println("Starting Light Status node..") - if err = statusAPI.StartNode(config); err != nil { - return err - } - - // wait till node has been stopped - node, err := statusAPI.NodeManager().Node() - if err != nil { - return nil - } - node.Wait() - - return nil -} - -// parseLESCommandConfig parses incoming CLI options and returns node configuration object -func parseLESCommandConfig(ctx *cli.Context) (*params.NodeConfig, error) { - nodeConfig, err := makeNodeConfig(ctx) - if err != nil { - return nil, err - } - - // Enabled sub-protocols - nodeConfig.LightEthConfig.Enabled = true - nodeConfig.RPCEnabled = ctx.Bool(HTTPEnabledFlag.Name) - nodeConfig.WhisperConfig.Enabled = ctx.Bool(WhisperEnabledFlag.Name) - nodeConfig.SwarmConfig.Enabled = ctx.Bool(SwarmEnabledFlag.Name) - - // RPC configuration - if !ctx.Bool(HTTPEnabledFlag.Name) { - nodeConfig.HTTPHost = "" // HTTP RPC is disabled - } - nodeConfig.HTTPPort = ctx.Int(HTTPPortFlag.Name) - nodeConfig.IPCEnabled = ctx.Bool(IPCEnabledFlag.Name) - - return nodeConfig, nil -} diff --git a/cmd/statusd/main.go b/cmd/statusd/main.go index 6f2bdd3e0..d6c568bd9 100644 --- a/cmd/statusd/main.go +++ b/cmd/statusd/main.go @@ -1,174 +1,142 @@ package main import ( + "flag" "fmt" + "log" "os" - "path/filepath" "runtime" + "strings" "github.com/status-im/status-go/geth/api" "github.com/status-im/status-go/geth/params" - "gopkg.in/urfave/cli.v1" ) var ( - gitCommit = "rely on linker: -ldflags -X main.GitCommit" - buildStamp = "rely on linker: -ldflags -X main.buildStamp" - app = makeApp(gitCommit) - statusAPI = api.NewStatusAPI() + gitCommit = "N/A" // rely on linker: -ldflags -X main.GitCommit" + buildStamp = "N/A" // rely on linker: -ldflags -X main.buildStamp" ) var ( - // ProdModeFlag is whether we need dev or production settings - ProdModeFlag = cli.BoolFlag{ - Name: "production", - Usage: "Whether production settings should be loaded", - } - - // NodeKeyFileFlag is a node key file to be used as node's private key - NodeKeyFileFlag = cli.StringFlag{ - Name: "nodekey", - Usage: "P2P node key file (private key)", - } - - // DataDirFlag defines data directory for the node - DataDirFlag = cli.StringFlag{ - Name: "datadir", - Usage: "Data directory for the databases and keystore", - Value: params.DataDir, - } - - // NetworkIDFlag defines network ID - NetworkIDFlag = cli.IntFlag{ - Name: "networkid", - Usage: "Network identifier (integer, 1=Homestead, 3=Ropsten, 4=Rinkeby)", - Value: params.RopstenNetworkID, - } - - // WhisperEnabledFlag flags whether Whisper is enabled or not - WhisperEnabledFlag = cli.BoolFlag{ - Name: "shh", - Usage: "SHH protocol enabled", - } - - // SwarmEnabledFlag flags whether Swarm is enabled or not - SwarmEnabledFlag = cli.BoolFlag{ - Name: "swarm", - Usage: "Swarm protocol enabled", - } - - // HTTPEnabledFlag defines whether HTTP RPC endpoint should be opened or not - HTTPEnabledFlag = cli.BoolFlag{ - Name: "http", - Usage: "HTTP RPC enpoint enabled (default: false)", - } - - // HTTPPortFlag defines HTTP RPC port to use (if HTTP RPC is enabled) - HTTPPortFlag = cli.IntFlag{ - Name: "httpport", - Usage: "HTTP RPC server's listening port", - Value: params.HTTPPort, - } - - // IPCEnabledFlag flags whether IPC is enabled or not - IPCEnabledFlag = cli.BoolFlag{ - Name: "ipc", - Usage: "IPC RPC enpoint enabled", - } - - // LogLevelFlag defines a log reporting level - LogLevelFlag = cli.StringFlag{ - Name: "log", - Usage: `Log level, one of: "ERROR", "WARN", "INFO", "DEBUG", and "TRACE"`, - Value: "", - } - - // LogFileFlag defines a log filename - LogFileFlag = cli.StringFlag{ - Name: "logfile", - Usage: `Path to the log file`, - Value: "", - } + prodMode = flag.Bool("production", false, "Whether production settings should be loaded") + nodeKeyFile = flag.String("nodekey", "", "P2P node key file (private key)") + dataDir = flag.String("datadir", params.DataDir, "Data directory for the databases and keystore") + networkID = flag.Int("networkid", params.RopstenNetworkID, "Network identifier (integer, 1=Homestead, 3=Ropsten, 4=Rinkeby, 777=StatusChain)") + whisperEnabled = flag.Bool("shh", false, "SHH protocol enabled") + swarmEnabled = flag.Bool("swarm", false, "Swarm protocol enabled") + httpEnabled = flag.Bool("http", false, "HTTP RPC endpoint enabled (default: false)") + httpPort = flag.Int("httpport", params.HTTPPort, "HTTP RPC server's listening port") + ipcEnabled = flag.Bool("ipc", false, "IPC RPC endpoint enabled") + logLevel = flag.String("log", "INFO", `Log level, one of: "ERROR", "WARN", "INFO", "DEBUG", and "TRACE"`) + logFile = flag.String("logfile", "", "Path to the log file") + version = flag.Bool("version", false, "Print version") ) -func init() { - // setup the app - app.Action = cli.ShowAppHelp - app.HideVersion = true // separate command prints version - app.Commands = []cli.Command{ - versionCommand, - faucetCommand, - lesCommand, - wnodeCommand, - } - app.Flags = []cli.Flag{ - ProdModeFlag, - NodeKeyFileFlag, - DataDirFlag, - NetworkIDFlag, - LogLevelFlag, - LogFileFlag, - } - app.Before = func(ctx *cli.Context) error { - runtime.GOMAXPROCS(runtime.NumCPU()) - return nil - } - app.After = func(ctx *cli.Context) error { - return nil - } -} - func main() { - if err := app.Run(os.Args); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } -} + flag.Usage = printUsage + flag.Parse() -// makeApp creates an app with sane defaults. -func makeApp(gitCommit string) *cli.App { - app := cli.NewApp() - app.Name = filepath.Base(os.Args[0]) - app.Author = "" - //app.Authors = nil - app.Email = "" - app.Version = params.Version - if gitCommit != "" { - app.Version += "-" + gitCommit[:8] + config, err := makeNodeConfig() + if err != nil { + log.Fatalf("Making config failed: %v", err) + return } - app.Usage = "CLI for Status nodes management" - return app + + if *version { + printVersion(config, gitCommit, buildStamp) + return + } + + backend := api.NewStatusBackend() + started, err := backend.StartNode(config) + if err != nil { + log.Fatalf("Node start failed: %v", err) + return + } + + // wait till node is started + <-started + + // wait till node has been stopped + node, err := backend.NodeManager().Node() + if err != nil { + log.Fatalf("Getting node failed: %v", err) + return + } + + node.Wait() } // makeNodeConfig parses incoming CLI options and returns node configuration object -func makeNodeConfig(ctx *cli.Context) (*params.NodeConfig, error) { - nodeConfig, err := params.NewNodeConfig( - ctx.GlobalString(DataDirFlag.Name), - ctx.GlobalUint64(NetworkIDFlag.Name), - !ctx.GlobalBool(ProdModeFlag.Name)) +func makeNodeConfig() (*params.NodeConfig, error) { + devMode := !*prodMode + nodeConfig, err := params.NewNodeConfig(*dataDir, uint64(*networkID), devMode) if err != nil { return nil, err } - nodeConfig.NodeKeyFile = ctx.GlobalString(NodeKeyFileFlag.Name) + // TODO(divan): move this logic into params package + if *nodeKeyFile != "" { + nodeConfig.NodeKeyFile = *nodeKeyFile + } - if logLevel := ctx.GlobalString(LogLevelFlag.Name); logLevel != "" { - nodeConfig.LogLevel = logLevel + if *logLevel != "" { + nodeConfig.LogLevel = *logLevel } - if logFile := ctx.GlobalString(LogFileFlag.Name); logFile != "" { - nodeConfig.LogFile = logFile + if *logFile != "" { + nodeConfig.LogFile = *logFile } + nodeConfig.LightEthConfig.Enabled = true + nodeConfig.RPCEnabled = *httpEnabled + nodeConfig.WhisperConfig.Enabled = *whisperEnabled + nodeConfig.SwarmConfig.Enabled = *swarmEnabled + + // RPC configuration + if !*httpEnabled { + nodeConfig.HTTPHost = "" // HTTP RPC is disabled + } + nodeConfig.HTTPPort = *httpPort + nodeConfig.IPCEnabled = *ipcEnabled + return nodeConfig, nil } -// printNodeConfig prints node config -func printNodeConfig(ctx *cli.Context) { - nodeConfig, err := makeNodeConfig(ctx) - if err != nil { - fmt.Printf("Loaded Config: failed (err: %v)", err) - return +// printVersion prints verbose output about version and config. +func printVersion(config *params.NodeConfig, gitCommit, buildStamp string) { + if gitCommit != "" && len(gitCommit) > 8 { + params.Version += "-" + gitCommit[:8] } - nodeConfig.LightEthConfig.Genesis = "SKIP" - fmt.Println("Loaded Config: ", nodeConfig) + + fmt.Println(strings.Title(params.ClientIdentifier)) + fmt.Println("Version:", params.Version) + if gitCommit != "" { + fmt.Println("Git Commit:", gitCommit) + } + if buildStamp != "" { + fmt.Println("Build Stamp:", buildStamp) + } + + fmt.Println("Network Id:", config.NetworkID) + fmt.Println("Go Version:", runtime.Version()) + fmt.Println("OS:", runtime.GOOS) + fmt.Printf("GOPATH=%s\n", os.Getenv("GOPATH")) + fmt.Printf("GOROOT=%s\n", runtime.GOROOT()) + + config.LightEthConfig.Genesis = "SKIP" + fmt.Println("Loaded Config: ", config) +} + +func printUsage() { + fmt.Fprintln(os.Stderr, "Usage: statusd [options]") + fmt.Fprintf(os.Stderr, ` +Examples: + statusd # run status node with defaults + statusd -networkid 4 # run node on Rinkeby network + statusd -datadir /dir # specify different dir for data + statusd -ipc # enable IPC for usage with "geth attach" + +Options: +`) + flag.PrintDefaults() } diff --git a/cmd/statusd/misccmd.go b/cmd/statusd/misccmd.go deleted file mode 100644 index 1559bf328..000000000 --- a/cmd/statusd/misccmd.go +++ /dev/null @@ -1,41 +0,0 @@ -package main - -import ( - "fmt" - "os" - "runtime" - "strings" - - "github.com/status-im/status-go/geth/params" - "gopkg.in/urfave/cli.v1" -) - -var ( - versionCommand = cli.Command{ - Action: versionCommandHandler, - Name: "version", - Usage: "Print app version", - } -) - -// versionCommandHandler displays app version -func versionCommandHandler(ctx *cli.Context) error { - fmt.Println(strings.Title(params.ClientIdentifier)) - fmt.Println("Version:", params.Version) - if gitCommit != "" { - fmt.Println("Git Commit:", gitCommit) - } - if buildStamp != "" { - fmt.Println("Build Stamp:", buildStamp) - } - - fmt.Println("Network Id:", ctx.GlobalInt(NetworkIDFlag.Name)) - fmt.Println("Go Version:", runtime.Version()) - fmt.Println("OS:", runtime.GOOS) - fmt.Printf("GOPATH=%s\n", os.Getenv("GOPATH")) - fmt.Printf("GOROOT=%s\n", runtime.GOROOT()) - - printNodeConfig(ctx) - - return nil -} diff --git a/cmd/statusd/wnodecmd.go b/cmd/statusd/wnodecmd.go deleted file mode 100644 index 80f6f77ef..000000000 --- a/cmd/statusd/wnodecmd.go +++ /dev/null @@ -1,269 +0,0 @@ -package main - -import ( - "errors" - "fmt" - "path/filepath" - - "github.com/status-im/status-go/geth/account" - "github.com/status-im/status-go/geth/common" - "github.com/status-im/status-go/geth/params" - "gopkg.in/urfave/cli.v1" -) - -var ( - // WhisperEchoModeFlag enables/disables Echo mode (arguments are printed for diagnostics) - WhisperEchoModeFlag = cli.BoolTFlag{ - Name: "echo", - Usage: "Echo mode, prints some arguments for diagnostics (default: true)", - } - - // WhisperBootstrapNodeFlag marks node as not actively listening for incoming connections - WhisperBootstrapNodeFlag = cli.BoolTFlag{ - Name: "bootstrap", - Usage: "Don't actively connect to peers, wait for incoming connections (default: true)", - } - - // WhisperNotificationServerNodeFlag enables/disables Push Notifications services - WhisperNotificationServerNodeFlag = cli.BoolFlag{ - Name: "notify", - Usage: "Node is capable of sending Push Notifications", - } - - // WhisperForwarderNodeFlag enables/disables message forwarding - // (when neither sends nor decrypts envelopes, just forwards them) - WhisperForwarderNodeFlag = cli.BoolFlag{ - Name: "forward", - Usage: "Only forward messages, neither send nor decrypt messages", - } - - // WhisperMailserverNodeFlag enables/disables Inboxing services - WhisperMailserverNodeFlag = cli.BoolFlag{ - Name: "mailserver", - Usage: "Delivers expired messages on demand", - } - - // WhisperIdentityFile is path to file containing private key of the node (for asymmetric encryption) - WhisperIdentityFile = cli.StringFlag{ - Name: "identity", - Usage: "Protocol identity file (private key used for asymmetric encryption)", - } - - // WhisperPasswordFile is password used to do a symmetric encryption - WhisperPasswordFile = cli.StringFlag{ - Name: "password", - Usage: "Password file (password is used for symmetric encryption)", - } - - // WhisperPortFlag defines port on which Whisper protocol is listening - WhisperPortFlag = cli.IntFlag{ - Name: "port", - Usage: "Whisper node's listening port", - Value: params.WhisperPort, - } - - // WhisperPoWFlag is the minimum PoW required by the node - WhisperPoWFlag = cli.Float64Flag{ - Name: "pow", - Usage: "PoW for messages to be added to queue, in float format", - Value: params.WhisperMinimumPoW, - } - - // WhisperTTLFlag defines node's default TTL for envelopes - WhisperTTLFlag = cli.IntFlag{ - Name: "ttl", - Usage: "Time to live for messages, in seconds", - Value: params.WhisperTTL, - } - - // WhisperInjectTestAccounts if set, then test accounts will be imported - // into node's key store, and then will be injected as key pairs (identities) - // into the Whisper as well. - WhisperInjectTestAccounts = cli.BoolTFlag{ - Name: "injectaccounts", - Usage: "Whether test account should be injected or not (default: true)", - } - - // FirebaseAuthorizationKey path to file containing FCM password - FirebaseAuthorizationKey = cli.StringFlag{ - Name: "firebaseauth", - Usage: "FCM Authorization Key used for sending Push Notifications", - } -) - -var ( - wnodeCommand = cli.Command{ - Action: wnode, - Name: "wnode", - Usage: "Starts Whisper/5 node", - Flags: []cli.Flag{ - WhisperEchoModeFlag, - WhisperBootstrapNodeFlag, - WhisperNotificationServerNodeFlag, - WhisperForwarderNodeFlag, - WhisperMailserverNodeFlag, - WhisperIdentityFile, - WhisperPasswordFile, - WhisperPoWFlag, - WhisperPortFlag, - WhisperTTLFlag, - WhisperInjectTestAccounts, - FirebaseAuthorizationKey, - HTTPEnabledFlag, - HTTPPortFlag, - }, - } -) - -// version displays app version -func wnode(ctx *cli.Context) error { - config, err := makeWhisperNodeConfig(ctx) - if err != nil { - return fmt.Errorf("can not parse config: %v", err) - } - - wnodePrintHeader(config) - - // import test accounts - if ctx.BoolT(WhisperInjectTestAccounts.Name) { - if err = common.ImportTestAccount(filepath.Join(config.DataDir, "keystore"), "test-account1.pk"); err != nil { - return err - } - if err = common.ImportTestAccount(filepath.Join(config.DataDir, "keystore"), "test-account2.pk"); err != nil { - return err - } - } - if err = statusAPI.StartNode(config); err != nil { - return err - } - - // inject test accounts into Whisper - if ctx.BoolT(WhisperInjectTestAccounts.Name) { - testConfig, _ := common.LoadTestConfig() - if err = injectAccountIntoWhisper(testConfig.Account1.Address, testConfig.Account1.Password); err != nil { - return err - } - if err = injectAccountIntoWhisper(testConfig.Account2.Address, testConfig.Account2.Password); err != nil { - return err - } - } - - // wait till node has been stopped - node, err := statusAPI.NodeManager().Node() - if err != nil { - return nil - } - node.Wait() - - return nil -} - -// wnodePrintHeader prints command header -func wnodePrintHeader(nodeConfig *params.NodeConfig) { - fmt.Println("Starting Whisper/5 node..") - - whisperConfig := nodeConfig.WhisperConfig - - if whisperConfig.EchoMode { - fmt.Printf("Whisper Config: %s\n", whisperConfig) - } -} - -// makeWhisperNodeConfig parses incoming CLI options and returns node configuration object -func makeWhisperNodeConfig(ctx *cli.Context) (*params.NodeConfig, error) { - nodeConfig, err := makeNodeConfig(ctx) - if err != nil { - return nil, err - } - - nodeConfig.LightEthConfig.Enabled = false - - whisperConfig := nodeConfig.WhisperConfig - - whisperConfig.Enabled = true - whisperConfig.IdentityFile = ctx.String(WhisperIdentityFile.Name) - whisperConfig.PasswordFile = ctx.String(WhisperPasswordFile.Name) - whisperConfig.EchoMode = ctx.BoolT(WhisperEchoModeFlag.Name) - whisperConfig.BootstrapNode = ctx.BoolT(WhisperBootstrapNodeFlag.Name) - whisperConfig.ForwarderNode = ctx.Bool(WhisperForwarderNodeFlag.Name) - whisperConfig.NotificationServerNode = ctx.Bool(WhisperNotificationServerNodeFlag.Name) - whisperConfig.MailServerNode = ctx.Bool(WhisperMailserverNodeFlag.Name) - whisperConfig.Port = ctx.Int(WhisperPortFlag.Name) - whisperConfig.TTL = ctx.Int(WhisperTTLFlag.Name) - whisperConfig.MinimumPoW = ctx.Float64(WhisperPoWFlag.Name) - - if whisperConfig.MailServerNode && len(whisperConfig.PasswordFile) == 0 { - return nil, errors.New("mail server requires --password to be specified") - } - - if whisperConfig.NotificationServerNode && len(whisperConfig.IdentityFile) == 0 { - return nil, errors.New("notification server requires either --identity file to be specified") - } - - if len(whisperConfig.PasswordFile) > 0 { // make sure that we can load password file - if whisperConfig.PasswordFile, err = filepath.Abs(whisperConfig.PasswordFile); err != nil { - return nil, err - } - if _, err = whisperConfig.ReadPasswordFile(); err != nil { - return nil, err - } - } - - if len(whisperConfig.IdentityFile) > 0 { // make sure that we can load identity file - if whisperConfig.IdentityFile, err = filepath.Abs(whisperConfig.IdentityFile); err != nil { - return nil, err - } - if _, err = whisperConfig.ReadIdentityFile(); err != nil { - return nil, err - } - } - - firebaseConfig := whisperConfig.FirebaseConfig - firebaseConfig.AuthorizationKeyFile = ctx.String(FirebaseAuthorizationKey.Name) - if len(firebaseConfig.AuthorizationKeyFile) > 0 { // make sure authorization key can be loaded - if firebaseConfig.AuthorizationKeyFile, err = filepath.Abs(firebaseConfig.AuthorizationKeyFile); err != nil { - return nil, err - } - if _, err := firebaseConfig.ReadAuthorizationKeyFile(); err != nil { - return nil, err - } - } - - // RPC configuration - if !ctx.Bool(HTTPEnabledFlag.Name) { - nodeConfig.HTTPHost = "" // HTTP RPC is disabled - } - nodeConfig.HTTPPort = ctx.Int(HTTPPortFlag.Name) - - return nodeConfig, nil -} - -// injectAccountIntoWhisper adds key pair into Whisper. Similar to Select/Login, -// but allows multiple accounts to be injected. -func injectAccountIntoWhisper(address, password string) error { - nodeManager := statusAPI.NodeManager() - keyStore, err := nodeManager.AccountKeyStore() - if err != nil { - return err - } - - acct, err := common.ParseAccountString(address) - if err != nil { - return account.ErrAddressToAccountMappingFailure - } - - _, accountKey, err := keyStore.AccountDecryptedKey(acct, password) - if err != nil { - return fmt.Errorf("%s: %v", account.ErrAccountToKeyMappingFailure.Error(), err) - } - - whisperService, err := nodeManager.WhisperService() - if err != nil { - return err - } - if _, err = whisperService.AddKeyPair(accountKey.PrivateKey); err != nil { - return err - } - - return nil -} diff --git a/cmd/wnode-status/config.go b/cmd/wnode-status/config.go new file mode 100644 index 000000000..8318e8f29 --- /dev/null +++ b/cmd/wnode-status/config.go @@ -0,0 +1,113 @@ +package main + +import ( + "errors" + "fmt" + "path/filepath" + + "github.com/status-im/status-go/geth/params" +) + +// makeNodeConfig creates node configuration object from flags +func makeNodeConfig() (*params.NodeConfig, error) { + devMode := !*prodMode + nodeConfig, err := params.NewNodeConfig(*dataDir, uint64(*networkID), devMode) + if err != nil { + return nil, err + } + + // TODO(divan): move this logic into params package? + if *nodeKeyFile != "" { + nodeConfig.NodeKeyFile = *nodeKeyFile + } + + // disable log + nodeConfig.LogLevel = "CRIT" + nodeConfig.LogFile = "" + + // disable les and swarm for wnode + nodeConfig.LightEthConfig.Enabled = false + nodeConfig.SwarmConfig.Enabled = false + + nodeConfig.RPCEnabled = *httpEnabled + + // whisper configuration + whisperConfig := nodeConfig.WhisperConfig + + whisperConfig.Enabled = true + whisperConfig.IdentityFile = *identity + whisperConfig.PasswordFile = *password + whisperConfig.EchoMode = *echo + whisperConfig.BootstrapNode = *bootstrap + whisperConfig.ForwarderNode = *forward + whisperConfig.NotificationServerNode = *notify + whisperConfig.MailServerNode = *mailserver + whisperConfig.Port = *port + whisperConfig.TTL = *ttl + whisperConfig.MinimumPoW = *pow + + if whisperConfig.MailServerNode && whisperConfig.PasswordFile == "" { + return nil, errors.New("mail server requires -password to be specified") + } + + if whisperConfig.NotificationServerNode && whisperConfig.IdentityFile == "" { + return nil, errors.New("notification server requires either -identity file to be specified") + } + + if whisperConfig.PasswordFile != "" { + if err := verifyPasswordFile(whisperConfig); err != nil { + return nil, fmt.Errorf("read password file: %v", err) + } + } + + if whisperConfig.IdentityFile != "" { + if err := verifyIdentityFile(whisperConfig); err != nil { + return nil, fmt.Errorf("read identity file: %v", err) + } + } + + // firebase configuration + firebaseConfig := whisperConfig.FirebaseConfig + firebaseConfig.AuthorizationKeyFile = *firebaseAuth + if len(firebaseConfig.AuthorizationKeyFile) > 0 { // make sure authorization key can be loaded + if firebaseConfig.AuthorizationKeyFile, err = filepath.Abs(firebaseConfig.AuthorizationKeyFile); err != nil { + return nil, err + } + if _, err := firebaseConfig.ReadAuthorizationKeyFile(); err != nil { + return nil, err + } + } + + // RPC configuration + if !*httpEnabled { + nodeConfig.HTTPHost = "" // HTTP RPC is disabled + } + nodeConfig.HTTPPort = *httpPort + nodeConfig.IPCEnabled = *ipcEnabled + + return nodeConfig, nil +} + +// verifyPasswordFile verifies that we can load password file +func verifyPasswordFile(config *params.WhisperConfig) error { + // TODO(divan): why do we need it here? + absPath, err := filepath.Abs(config.PasswordFile) + if err != nil { + return err + } + config.PasswordFile = absPath + _, err = config.ReadPasswordFile() + return err +} + +// verifyIdentityFile verifies that we can load identity file +func verifyIdentityFile(config *params.WhisperConfig) error { + // TODO(divan): why do we need it here? + absPath, err := filepath.Abs(config.IdentityFile) + if err != nil { + return err + } + config.IdentityFile = absPath + _, err = config.ReadIdentityFile() + return err +} diff --git a/cmd/wnode-status/main.go b/cmd/wnode-status/main.go new file mode 100644 index 000000000..7a1183368 --- /dev/null +++ b/cmd/wnode-status/main.go @@ -0,0 +1,84 @@ +package main + +import ( + "flag" + "fmt" + "log" + + "github.com/status-im/status-go/geth/api" + "github.com/status-im/status-go/geth/params" +) + +var ( + prodMode = flag.Bool("production", false, "Whether production settings should be loaded") + nodeKeyFile = flag.String("nodekey", "", "P2P node key file (private key)") + dataDir = flag.String("datadir", "wnode-status-data", "Data directory for the databases and keystore") + networkID = flag.Int("networkid", params.RopstenNetworkID, "Network identifier (integer, 1=Homestead, 3=Ropsten, 4=Rinkeby)") + httpEnabled = flag.Bool("http", false, "HTTP RPC enpoint enabled (default: false)") + httpPort = flag.Int("httpport", params.HTTPPort, "HTTP RPC server's listening port") + ipcEnabled = flag.Bool("ipc", false, "IPC RPC enpoint enabled") + + // wnode specific flags + echo = flag.Bool("echo", true, "Echo mode, prints some arguments for diagnostics") + bootstrap = flag.Bool("bootstrap", true, "Don't actively connect to peers, wait for incoming connections") + notify = flag.Bool("notify", false, "Node is capable of sending Push Notifications") + forward = flag.Bool("forward", false, "Only forward messages, neither send nor decrypt messages") + mailserver = flag.Bool("mailserver", false, "Delivers expired messages on demand") + identity = flag.String("identity", "", "Protocol identity file (private key used for asymmetric encryption)") + password = flag.String("password", "", "Password file (password is used for symmetric encryption)") + port = flag.Int("port", params.WhisperPort, "Whisper node's listening port") + pow = flag.Float64("pow", params.WhisperMinimumPoW, "PoW for messages to be added to queue, in float format") + ttl = flag.Int("ttl", params.WhisperTTL, "Time to live for messages, in seconds") + injectAccounts = flag.Bool("injectaccounts", true, "Whether test account should be injected or not") + firebaseAuth = flag.String("firebaseauth", "", "FCM Authorization Key used for sending Push Notifications") +) + +func main() { + flag.Parse() + + config, err := makeNodeConfig() + if err != nil { + log.Fatalf("Making config failed: %v", err) + } + + printHeader(config) + + if *injectAccounts { + if err := LoadTestAccounts(config.DataDir); err != nil { + log.Fatalf("Failed to load test accounts: %v", err) + } + } + + backend := api.NewStatusBackend() + started, err := backend.StartNode(config) + if err != nil { + log.Fatalf("Node start failed: %v", err) + return + } + + // wait till node is started + <-started + + if *injectAccounts { + if err := InjectTestAccounts(backend.NodeManager()); err != nil { + log.Fatalf("Failed to inject accounts: %v", err) + } + } + + // wait till node has been stopped + node, err := backend.NodeManager().Node() + if err != nil { + log.Fatalf("Getting node failed: %v", err) + return + } + + node.Wait() +} + +// printHeader prints command header +func printHeader(config *params.NodeConfig) { + fmt.Println("Starting Whisper/5 node..") + if config.WhisperConfig.EchoMode { + fmt.Printf("Whisper Config: %s\n", config.WhisperConfig) + } +} diff --git a/cmd/wnode-status/test_accounts.go b/cmd/wnode-status/test_accounts.go new file mode 100644 index 000000000..18d3053fa --- /dev/null +++ b/cmd/wnode-status/test_accounts.go @@ -0,0 +1,69 @@ +package main + +import ( + "fmt" + "path/filepath" + + "github.com/status-im/status-go/geth/account" + "github.com/status-im/status-go/geth/common" +) + +// LoadTestAccounts loads public key files for test accounts +func LoadTestAccounts(dataDir string) error { + files := []string{"test-account1.pk", "test-account2.pk"} + dir := filepath.Join(dataDir, "keystore") + for _, filename := range files { + if err := common.ImportTestAccount(dir, filename); err != nil { + return err + } + } + return nil +} + +// InjectTestAccounts injects test accounts into running node +func InjectTestAccounts(node common.NodeManager) error { + testConfig, err := common.LoadTestConfig() + if err != nil { + return err + } + + if err = injectAccountIntoWhisper(node, testConfig.Account1.Address, + testConfig.Account1.Password); err != nil { + return err + } + if err = injectAccountIntoWhisper(node, testConfig.Account2.Address, + testConfig.Account2.Password); err != nil { + return err + } + + return nil +} + +// injectAccountIntoWhisper adds key pair into Whisper. Similar to Select/Login, +// but allows multiple accounts to be injected. +func injectAccountIntoWhisper(node common.NodeManager, address, password string) error { + keyStore, err := node.AccountKeyStore() + if err != nil { + return err + } + + acct, err := common.ParseAccountString(address) + if err != nil { + return account.ErrAddressToAccountMappingFailure + } + + _, accountKey, err := keyStore.AccountDecryptedKey(acct, password) + if err != nil { + return fmt.Errorf("%s: %v", account.ErrAccountToKeyMappingFailure.Error(), err) + } + + whisperService, err := node.WhisperService() + if err != nil { + return err + } + if _, err = whisperService.AddKeyPair(accountKey.PrivateKey); err != nil { + return err + } + + return nil +} diff --git a/cmd/statusd/library.go b/lib/library.go similarity index 100% rename from cmd/statusd/library.go rename to lib/library.go diff --git a/cmd/statusd/library_test.go b/lib/library_test.go similarity index 100% rename from cmd/statusd/library_test.go rename to lib/library_test.go diff --git a/lib/main.go b/lib/main.go new file mode 100644 index 000000000..2d7372926 --- /dev/null +++ b/lib/main.go @@ -0,0 +1,13 @@ +package main + +import "github.com/status-im/status-go/geth/api" + +var statusAPI = api.NewStatusAPI() + +// Technically this package supposed to be a lib for +// cross-compilation and usage with Android/iOS, but +// without main it produces cryptic errors. +// TODO(divan): investigate the cause of the errors +// and change this package to be a library if possible. +func main() { +} diff --git a/cmd/statusd/utils.go b/lib/utils.go similarity index 100% rename from cmd/statusd/utils.go rename to lib/utils.go diff --git a/static/tests/whisper.js b/static/tests/whisper.js index 97a33dbf4..87ab32f6f 100644 --- a/static/tests/whisper.js +++ b/static/tests/whisper.js @@ -10,9 +10,9 @@ describe('Whisper Tests', function () { node1.setProvider(new web3.providers.HttpProvider('http://localhost:8645')); node2.setProvider(new web3.providers.HttpProvider('http://localhost:8745')); - console.log('Node is expected: statusd --datadir app1 wnode --http --httpport 8645'); - console.log('Node is expected: statusd --datadir app2 wnode --http --httpport 8745'); - console.log('Node is expected: statusd --datadir wnode1 wnode --notify --injectaccounts=false --identity ./static/keys/wnodekey --firebaseauth ./static/keys/firebaseauthkey'); + console.log('Node is expected: wnode-status -datadir app1 wnode -http -httpport 8645'); + console.log('Node is expected: wnode-status -datadir app2 wnode -http -httpport 8745'); + console.log('Node is expected: wnode-status -datadir wnode1 wnode -notify -injectaccounts=false -identity ./static/keys/wnodekey -firebaseauth ./static/keys/firebaseauthkey'); // some common vars var topic1 = '0xdeadbeef'; // each topic 4 bytes, as hex