package main import ( "context" "crypto/ecdsa" "encoding/hex" "flag" "fmt" "log" "math" "os" ossignal "os/signal" "path/filepath" "strings" "syscall" "time" "github.com/google/uuid" "github.com/jroimartin/gocui" "github.com/peterbourgon/ff" "github.com/pkg/errors" "github.com/status-im/status-go/eth-node/crypto" "github.com/status-im/status-go/eth-node/types" "github.com/status-im/status-go/logutils" "github.com/status-im/status-go/params" "github.com/status-im/status-go/protocol" "github.com/status-im/status-go/protocol/zaputil" "github.com/status-im/status-go/signal" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) var g *gocui.Gui var ( fs = flag.NewFlagSet("status-term-client", flag.ExitOnError) logLevel = fs.String("log-level", "INFO", "log level") keyHex = fs.String("keyhex", "", "pass a private key in hex") noUI = fs.Bool("no-ui", false, "disable UI") // flags acting like commands createKeyPair = fs.Bool("create-key-pair", false, "creates and prints a key pair instead of running") // flags for in-proc node dataDir = fs.String("data-dir", filepath.Join(os.TempDir(), "status-term-client"), "data directory for Ethereum node") installationID = fs.String("installation-id", uuid.New().String(), "the installationID to be used") noNamespace = fs.Bool("no-namespace", false, "disable data dir namespacing with public key") fleet = fs.String("fleet", params.FleetBeta, fmt.Sprintf("Status nodes cluster to connect to: %s", []string{params.FleetBeta, params.FleetStaging})) configFile = fs.String("node-config", "", "a JSON file with node config") listenAddr = fs.String("listen-addr", ":30303", "The address the Ethereum node should be listening to") datasync = fs.Bool("datasync", false, "enable datasync") sendV1Messages = fs.Bool("send-v1-messages", false, "enable sending v1 compatible only messages") genericDiscoveryTopic = fs.Bool("generic-discovery-topic", true, "enable generic discovery topic, for compatibility with pre-v1") // flags for external node providerURI = fs.String("provider", "", "an URI pointing at a provider") useNimbus = fs.Bool("nimbus", false, "use Nimbus node") ) func main() { if err := ff.Parse(fs, os.Args[1:]); err != nil { exitErr(errors.Wrap(err, "failed to parse flags")) } if *createKeyPair { key, err := crypto.GenerateKey() if err != nil { exitErr(err) } fmt.Printf("Your private key: %#x\n", crypto.FromECDSA(key)) os.Exit(0) } var privateKey *ecdsa.PrivateKey if *keyHex != "" { k, err := crypto.HexToECDSA(strings.TrimPrefix(*keyHex, "0x")) if err != nil { exitErr(err) } privateKey = k } else { k, err := crypto.GenerateKey() if err != nil { exitErr(err) } privateKey = k fmt.Printf("Starting with a new private key: %#x\n", crypto.FromECDSA(privateKey)) } if *useNimbus { if err := startNimbus(privateKey, *listenAddr, *fleet == params.FleetStaging); err != nil { exitErr(err) } } // Prefix data directory with a public key. // This is required because it's not possible // or advised to share data between different // key pairs. if !*noNamespace { *dataDir = filepath.Join( *dataDir, hex.EncodeToString(crypto.FromECDSAPub(&privateKey.PublicKey)[:20]), ) } err := os.MkdirAll(*dataDir, 0755) if err != nil { exitErr(err) } else { fmt.Printf("Starting in %s\n", *dataDir) } // Setup logging by splitting it into a client.log // with status-console-client logs and status.log // with Status Node logs. clientLogPath := filepath.Join(*dataDir, "client.log") clientLogFile, err := os.OpenFile(clientLogPath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644) if err != nil { exitErr(err) } // Forward standard logger output. log.SetOutput(clientLogFile) // Create zap logger. if err := zaputil.RegisterJSONHexEncoder(); err != nil { exitErr(err) } cfg := zap.NewProductionConfig() cfg.Level = zap.NewAtomicLevelAt(zapcore.InfoLevel) cfg.OutputPaths = []string{clientLogFile.Name()} cfg.Encoding = "json-hex" logger, err := cfg.Build() if err != nil { exitErr(fmt.Errorf("failed to create logger: %v", err)) } // Status node logs. nodeLogPath := filepath.Join(*dataDir, "status.log") err = logutils.OverrideRootLog(true, *logLevel, logutils.FileOptions{Filename: nodeLogPath}, false) if err != nil { exitErr(fmt.Errorf("failed to override root log: %v", err)) } // initialize protocol var ( messenger *protocol.Messenger pollFunc func() ) if *providerURI != "" { messenger, err = createMessengerWithURI(*providerURI) if err != nil { exitErr(err) } } else { messengerDBPath := filepath.Join(*dataDir, "messenger.sql") messenger, pollFunc, err = createMessengerInProc(privateKey, messengerDBPath, logger) if err != nil { exitErr(err) } } done := make(chan bool, 1) sigs := make(chan os.Signal, 1) ossignal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) go func() { sig := <-sigs logger.Error("received signal", zap.String("signal", sig.String())) done <- true }() logger.Info("starting UI...") if *noUI { <-done return } go func() { <-done exitErr(errors.New("exit with signal")) }() if err := setupGUI(privateKey, messenger, logger); err != nil { exitErr(err) } if err := messenger.Init(); err != nil { exitErr(err) } cancelPolling := make(chan struct{}, 1) startPolling(g, pollFunc, 50*time.Millisecond, cancelPolling) defer close(cancelPolling) if err := g.MainLoop(); err != nil && err != gocui.ErrQuit { close(cancelPolling) exitErr(err) } g.Close() } func exitErr(err error) { if g != nil { g.Close() } fmt.Println(err) os.Exit(1) } type keysGetter struct { privateKey *ecdsa.PrivateKey } func (k keysGetter) PrivateKey() (*ecdsa.PrivateKey, error) { return k.privateKey, nil } func createMessengerInProc(pk *ecdsa.PrivateKey, dbPath string, logger *zap.Logger) (*protocol.Messenger, func(), error) { // collect mail server request signals signalsForwarder := newSignalForwarder() go signalsForwarder.Start() // setup signals handler signal.SetDefaultNodeNotificationHandler( filterMailTypesHandler(signalsForwarder.in), ) var ( node types.Node err error ) if *useNimbus { node = newNimbusNodeWrapper() } else { if node, err = newGethNodeWrapper(pk); err != nil { exitErr(err) } } options := []protocol.Option{ protocol.WithCustomLogger(logger), protocol.WithDatabaseConfig(dbPath, ""), protocol.WithMessagesPersistenceEnabled(), } if *genericDiscoveryTopic { options = append(options, protocol.WithGenericDiscoveryTopicSupport()) } if *datasync { options = append(options, protocol.WithDatasync()) } if *sendV1Messages { options = append(options, protocol.WithSendV1Messages()) } messenger, err := protocol.NewMessenger( pk, node, *installationID, options..., ) if err != nil { return nil, nil, errors.Wrap(err, "failed to create Messenger") } if err := messenger.Init(); err != nil { return nil, nil, err } // protocolGethService.SetMessenger(messenger) var whisper types.Whisper if whisper, err = node.GetWhisper(nil); err != nil { exitErr(err) } return messenger, whisper.Poll, nil } func setupGUI(privateKey *ecdsa.PrivateKey, messenger *protocol.Messenger, logger *zap.Logger) error { var err error // global g, err = gocui.NewGui(gocui.Output256) if err != nil { return err } // prepare views vm := NewViewManager(nil, g, logger) notifications := NewNotificationViewController(&ViewController{vm, g, ViewNotification}) chatsVC := NewChatsViewController(&ViewController{vm, g, ViewChats}, messenger, logger) if err := chatsVC.LoadAndRefresh(); err != nil { return errors.Wrap(err, "failed to load chats") } messagesVC := NewMessagesViewController( &ViewController{vm, g, ViewChat}, privateKey, messenger, logger, func() { if err := chatsVC.LoadAndRefresh(); err != nil { logger.Error("failed to load and refresh chats", zap.Error(err)) } }, func(err error) { _ = notifications.Error("Chat error", fmt.Sprintf("%v", err)) }, ) messagesVC.Start() inputMultiplexer := NewInputMultiplexer() inputMultiplexer.AddHandler(DefaultMultiplexerPrefix, func(b []byte) error { logger.Info("default multiplexer handler") ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() _, err := messagesVC.Send(ctx, b) return err }) inputMultiplexer.AddHandler("/chat", ChatCmdFactory(chatsVC, messagesVC)) // inputMultiplexer.AddHandler("/request", RequestCmdFactory(chatVC)) views := []*View{ { Name: ViewChats, Enabled: true, Cursor: true, Highlight: true, SelBgColor: gocui.ColorGreen, SelFgColor: gocui.ColorBlack, TopLeft: func(maxX, maxY int) (int, int) { return 0, 0 }, BottomRight: func(maxX, maxY int) (int, int) { return int(math.Floor(float64(maxX) * 0.2)), maxY - 4 }, Keybindings: []Binding{ { Key: gocui.KeyArrowDown, Mod: gocui.ModNone, Handler: CursorDownHandler, }, { Key: gocui.KeyArrowUp, Mod: gocui.ModNone, Handler: CursorUpHandler, }, { Key: gocui.KeyEnter, Mod: gocui.ModNone, Handler: GetLineHandler(func(idx int, _ string) error { selectedChat, ok := chatsVC.ChatByIdx(idx) if !ok { return errors.New("chat could not be found") } // We need to call Select asynchronously, // otherwise the main thread is blocked // and nothing is rendered. go func() { messagesVC.Select(selectedChat) }() return nil }), }, }, }, { Name: ViewChat, Enabled: true, Cursor: true, Autoscroll: false, Highlight: true, Wrap: true, SelBgColor: gocui.ColorGreen, SelFgColor: gocui.ColorBlack, TopLeft: func(maxX, maxY int) (int, int) { return int(math.Ceil(float64(maxX) * 0.2)), 0 }, BottomRight: func(maxX, maxY int) (int, int) { return maxX - 1, maxY - 4 }, Keybindings: []Binding{ { Key: gocui.KeyArrowDown, Mod: gocui.ModNone, Handler: CursorDownHandler, }, { Key: gocui.KeyArrowUp, Mod: gocui.ModNone, Handler: CursorUpHandler, }, { Key: gocui.KeyHome, Mod: gocui.ModNone, Handler: func(g *gocui.Gui, v *gocui.View) error { return HomeHandler(g, v) }, }, { Key: gocui.KeyEnd, Mod: gocui.ModNone, Handler: EndHandler, }, }, }, { Name: ViewInput, Title: fmt.Sprintf( "%s (as %#x)", ViewInput, crypto.FromECDSAPub(&privateKey.PublicKey), ), Enabled: true, Editable: true, Cursor: true, Highlight: true, TopLeft: func(maxX, maxY int) (int, int) { return 0, maxY - 3 }, BottomRight: func(maxX, maxY int) (int, int) { return maxX - 1, maxY - 1 }, Keybindings: []Binding{ { Key: gocui.KeyEnter, Mod: gocui.ModNone, Handler: inputMultiplexer.BindingHandler, }, { Key: gocui.KeyEnter, Mod: gocui.ModAlt, Handler: MoveToNewLineHandler, }, }, }, { Name: ViewNotification, Enabled: false, Editable: false, Cursor: false, Highlight: true, TopLeft: func(maxX, maxY int) (int, int) { return maxX/2 - 50, maxY / 2 }, BottomRight: func(maxX, maxY int) (int, int) { return maxX/2 + 50, maxY/2 + 2 }, Keybindings: []Binding{ { Key: gocui.KeyEnter, Mod: gocui.ModNone, Handler: func(g *gocui.Gui, v *gocui.View) error { logger.Info("Notification Enter binding") if err := vm.DisableView(ViewNotification); err != nil { return err } if err := vm.DeleteView(ViewNotification); err != nil { return err } return nil }, }, }, }, } bindings := []Binding{ { Key: gocui.KeyCtrlC, Mod: gocui.ModNone, Handler: QuitHandler, }, { Key: gocui.KeyTab, Mod: gocui.ModNone, Handler: NextViewHandler(vm), }, } if err := vm.SetViews(views); err != nil { return err } if err := vm.SetGlobalKeybindings(bindings); err != nil { return err } return nil }