github.com/status-im/status-go@v1.1.0/cmd/spiff-workflow/main.go (about)

     1  package main
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"flag"
     7  	"fmt"
     8  	stdlog "log"
     9  	"os"
    10  	"os/signal"
    11  	"path/filepath"
    12  	"runtime"
    13  	"strings"
    14  	"time"
    15  
    16  	"github.com/google/uuid"
    17  	"golang.org/x/crypto/ssh/terminal"
    18  
    19  	"github.com/ethereum/go-ethereum/log"
    20  	gethmetrics "github.com/ethereum/go-ethereum/metrics"
    21  
    22  	"github.com/status-im/status-go/account/generator"
    23  	"github.com/status-im/status-go/api"
    24  	"github.com/status-im/status-go/common/dbsetup"
    25  	"github.com/status-im/status-go/eth-node/types"
    26  	"github.com/status-im/status-go/logutils"
    27  	"github.com/status-im/status-go/metrics"
    28  	nodemetrics "github.com/status-im/status-go/metrics/node"
    29  	"github.com/status-im/status-go/multiaccounts"
    30  	"github.com/status-im/status-go/multiaccounts/accounts"
    31  	"github.com/status-im/status-go/multiaccounts/settings"
    32  	"github.com/status-im/status-go/node"
    33  	"github.com/status-im/status-go/params"
    34  	"github.com/status-im/status-go/profiling"
    35  	"github.com/status-im/status-go/protocol"
    36  	"github.com/status-im/status-go/protocol/identity/alias"
    37  	waku2extn "github.com/status-im/status-go/services/wakuv2ext"
    38  )
    39  
    40  const (
    41  	serverClientName = "Statusd"
    42  )
    43  
    44  var (
    45  	configFiles      configFlags
    46  	logLevel         = flag.String("log", "INFO", `Log level, one of: "ERROR", "WARN", "INFO", "DEBUG", and "TRACE"`)
    47  	logWithoutColors = flag.Bool("log-without-color", false, "Disables log colors")
    48  	seedPhrase       = flag.String("seed-phrase", "", "Seed phrase")
    49  	version          = flag.Bool("version", false, "Print version and dump configuration")
    50  	apiModules       = flag.String("api-modules", "wakuext,ext,waku,ens", "API modules to enable in the HTTP server")
    51  	pprofEnabled     = flag.Bool("pprof", false, "Enable runtime profiling via pprof")
    52  	pprofPort        = flag.Int("pprof-port", 52525, "Port for runtime profiling via pprof")
    53  	metricsEnabled   = flag.Bool("metrics", false, "Expose ethereum metrics with debug_metrics jsonrpc call")
    54  	metricsPort      = flag.Int("metrics-port", 9305, "Port for the Prometheus /metrics endpoint")
    55  
    56  	dataDir   = flag.String("dir", getDefaultDataDir(), "Directory used by node to store data")
    57  	networkID = flag.Int(
    58  		"network-id",
    59  		params.GoerliNetworkID,
    60  		fmt.Sprintf(
    61  			"A network ID: %d (Mainnet), %d (Goerli)",
    62  			params.MainNetworkID, params.GoerliNetworkID,
    63  		),
    64  	)
    65  	listenAddr = flag.String("addr", "", "address to bind listener to")
    66  )
    67  
    68  // All general log messages in this package should be routed through this logger.
    69  var logger = log.New("package", "status-go/cmd/statusd")
    70  
    71  func init() {
    72  	flag.Var(&configFiles, "c", "JSON configuration file(s). Multiple configuration files can be specified, and will be merged in occurrence order")
    73  }
    74  
    75  // nolint:gocyclo
    76  func main() {
    77  	colors := terminal.IsTerminal(int(os.Stdin.Fd()))
    78  	if err := logutils.OverrideRootLog(true, "ERROR", logutils.FileOptions{}, colors); err != nil {
    79  		stdlog.Fatalf("Error initializing logger: %v", err)
    80  	}
    81  
    82  	flag.Usage = printUsage
    83  	flag.Parse()
    84  	if flag.NArg() > 0 {
    85  		printUsage()
    86  		logger.Error("Extra args in command line: %v", flag.Args())
    87  		os.Exit(1)
    88  	}
    89  
    90  	opts := []params.Option{}
    91  
    92  	config, err := params.NewNodeConfigWithDefaultsAndFiles(
    93  		*dataDir,
    94  		uint64(*networkID),
    95  		opts,
    96  		configFiles,
    97  	)
    98  	if err != nil {
    99  		printUsage()
   100  		logger.Error(err.Error())
   101  		os.Exit(1)
   102  	}
   103  
   104  	// Use listenAddr if and only if explicitly provided in the arguments.
   105  	// The default value is set in params.NewNodeConfigWithDefaultsAndFiles().
   106  	if *listenAddr != "" {
   107  		config.ListenAddr = *listenAddr
   108  	}
   109  
   110  	if *logLevel != "" {
   111  		config.LogLevel = *logLevel
   112  	}
   113  
   114  	// set up logging options
   115  	setupLogging(config)
   116  
   117  	// We want statusd to be distinct from StatusIM client.
   118  	config.Name = serverClientName
   119  
   120  	if *version {
   121  		printVersion(config)
   122  		return
   123  	}
   124  
   125  	// Check if profiling shall be enabled.
   126  	if *pprofEnabled {
   127  		profiling.NewProfiler(*pprofPort).Go()
   128  	}
   129  
   130  	backend := api.NewGethStatusBackend()
   131  	err = ImportAccount(*seedPhrase, backend)
   132  	if err != nil {
   133  		logger.Error("failed import account", "err", err)
   134  		return
   135  	}
   136  
   137  	// handle interrupt signals
   138  	interruptCh := exitOnInterruptSignal(backend.StatusNode())
   139  
   140  	// Start collecting metrics. Metrics can be enabled by providing `-metrics` flag
   141  	// or setting `gethmetrics.Enabled` to true during compilation time:
   142  	// https://github.com/status-im/go-ethereum/pull/76.
   143  	if *metricsEnabled || gethmetrics.Enabled {
   144  		go startNodeMetrics(interruptCh, backend.StatusNode())
   145  		go gethmetrics.CollectProcessMetrics(3 * time.Second)
   146  		go metrics.NewMetricsServer(*metricsPort, gethmetrics.DefaultRegistry).Listen()
   147  	}
   148  
   149  	wakuextservice := backend.StatusNode().WakuV2ExtService()
   150  	if wakuextservice == nil {
   151  		logger.Error("wakuext not available")
   152  		return
   153  	}
   154  
   155  	wakuext := waku2extn.NewPublicAPI(wakuextservice)
   156  
   157  	messenger := wakuext.Messenger()
   158  	messenger.DisableStoreNodes()
   159  	// This will start the push notification server as well as
   160  	// the config is set to Enabled
   161  	_, err = wakuext.StartMessenger()
   162  	if err != nil {
   163  		logger.Error("failed to start messenger", "error", err)
   164  		return
   165  	}
   166  
   167  	retrieveMessagesLoop(messenger, 300*time.Millisecond)
   168  
   169  }
   170  
   171  func getDefaultDataDir() string {
   172  	if home := os.Getenv("HOME"); home != "" {
   173  		return filepath.Join(home, ".statusd")
   174  	}
   175  	return "./statusd-data"
   176  }
   177  
   178  func setupLogging(config *params.NodeConfig) {
   179  	logSettings := logutils.LogSettings{
   180  		Enabled:         config.LogEnabled,
   181  		MobileSystem:    config.LogMobileSystem,
   182  		Level:           config.LogLevel,
   183  		File:            config.LogFile,
   184  		MaxSize:         config.LogMaxSize,
   185  		MaxBackups:      config.LogMaxBackups,
   186  		CompressRotated: config.LogCompressRotated,
   187  	}
   188  	colors := !(*logWithoutColors) && terminal.IsTerminal(int(os.Stdin.Fd()))
   189  	if err := logutils.OverrideRootLogWithConfig(logSettings, colors); err != nil {
   190  		stdlog.Fatalf("Error initializing logger: %v", err)
   191  	}
   192  }
   193  
   194  // printVersion prints verbose output about version and config.
   195  func printVersion(config *params.NodeConfig) {
   196  	fmt.Println(strings.Title(config.Name))
   197  	fmt.Println("Version:", config.Version)
   198  	fmt.Println("Network ID:", config.NetworkID)
   199  	fmt.Println("Go Version:", runtime.Version())
   200  	fmt.Println("OS:", runtime.GOOS)
   201  	fmt.Printf("GOPATH=%s\n", os.Getenv("GOPATH"))
   202  	fmt.Printf("GOROOT=%s\n", runtime.GOROOT())
   203  
   204  	fmt.Println("Loaded Config: ", config)
   205  }
   206  
   207  func printUsage() {
   208  	usage := `
   209  Usage: statusd [options]
   210  Examples:
   211    statusd                                        # run regular Whisper node that joins Status network
   212    statusd -c ./default.json                      # run node with configuration specified in ./default.json file
   213    statusd -c ./default.json -c ./standalone.json # run node with configuration specified in ./default.json file, after merging ./standalone.json file
   214    statusd -c ./default.json -metrics             # run node with configuration specified in ./default.json file, and expose ethereum metrics with debug_metrics jsonrpc call
   215  
   216  Options:
   217  `
   218  	fmt.Fprint(os.Stderr, usage)
   219  	flag.PrintDefaults()
   220  }
   221  
   222  const pathWalletRoot = "m/44'/60'/0'/0"
   223  const pathEIP1581 = "m/43'/60'/1581'"
   224  const pathDefaultChat = pathEIP1581 + "/0'/0"
   225  const pathDefaultWallet = pathWalletRoot + "/0"
   226  
   227  var paths = []string{pathWalletRoot, pathEIP1581, pathDefaultChat, pathDefaultWallet}
   228  
   229  func defaultSettings(generatedAccountInfo generator.GeneratedAccountInfo, derivedAddresses map[string]generator.AccountInfo, mnemonic *string) (*settings.Settings, error) {
   230  	chatKeyString := derivedAddresses[pathDefaultChat].PublicKey
   231  
   232  	defaultSettings := &settings.Settings{}
   233  	defaultSettings.KeyUID = generatedAccountInfo.KeyUID
   234  	defaultSettings.Address = types.HexToAddress(generatedAccountInfo.Address)
   235  	defaultSettings.WalletRootAddress = types.HexToAddress(derivedAddresses[pathWalletRoot].Address)
   236  
   237  	// Set chat key & name
   238  	name, err := alias.GenerateFromPublicKeyString(chatKeyString)
   239  	if err != nil {
   240  		return nil, err
   241  	}
   242  	defaultSettings.Name = name
   243  	defaultSettings.PublicKey = chatKeyString
   244  
   245  	defaultSettings.DappsAddress = types.HexToAddress(derivedAddresses[pathDefaultWallet].Address)
   246  	defaultSettings.EIP1581Address = types.HexToAddress(derivedAddresses[pathEIP1581].Address)
   247  	defaultSettings.Mnemonic = mnemonic
   248  
   249  	signingPhrase, err := buildSigningPhrase()
   250  	if err != nil {
   251  		return nil, err
   252  	}
   253  	defaultSettings.SigningPhrase = signingPhrase
   254  
   255  	defaultSettings.SendPushNotifications = true
   256  	defaultSettings.InstallationID = uuid.New().String()
   257  	defaultSettings.UseMailservers = true
   258  
   259  	defaultSettings.PreviewPrivacy = true
   260  	defaultSettings.PeerSyncingEnabled = false
   261  	defaultSettings.Currency = "usd"
   262  	defaultSettings.ProfilePicturesVisibility = settings.ProfilePicturesVisibilityEveryone
   263  	defaultSettings.LinkPreviewRequestEnabled = true
   264  
   265  	defaultSettings.TestNetworksEnabled = false
   266  
   267  	visibleTokens := make(map[string][]string)
   268  	visibleTokens["mainnet"] = []string{"SNT"}
   269  	visibleTokensJSON, err := json.Marshal(visibleTokens)
   270  	if err != nil {
   271  		return nil, err
   272  	}
   273  	visibleTokenJSONRaw := json.RawMessage(visibleTokensJSON)
   274  	defaultSettings.WalletVisibleTokens = &visibleTokenJSONRaw
   275  
   276  	// TODO: fix this
   277  	networks := make([]map[string]string, 0)
   278  	networksJSON, err := json.Marshal(networks)
   279  	if err != nil {
   280  		return nil, err
   281  	}
   282  	networkRawMessage := json.RawMessage(networksJSON)
   283  	defaultSettings.Networks = &networkRawMessage
   284  	defaultSettings.CurrentNetwork = "mainnet_rpc"
   285  
   286  	return defaultSettings, nil
   287  }
   288  
   289  func defaultNodeConfig(installationID string) (*params.NodeConfig, error) {
   290  	// Set mainnet
   291  	nodeConfig := &params.NodeConfig{}
   292  	nodeConfig.NetworkID = 1
   293  	nodeConfig.LogLevel = "DEBUG"
   294  	nodeConfig.DataDir = api.DefaultDataDir
   295  	nodeConfig.HTTPEnabled = true
   296  	nodeConfig.HTTPPort = 8545
   297  	// FIXME: This should be taken from CLI flags.
   298  	nodeConfig.HTTPHost = "0.0.0.0"
   299  	// FIXME: This should be taken from CLI flags.
   300  	nodeConfig.HTTPVirtualHosts = []string{"localhost", "wakunode"}
   301  	nodeConfig.APIModules = *apiModules
   302  	// Disable to avoid errors about empty ClusterConfig.BootNodes.
   303  	nodeConfig.NoDiscovery = true
   304  
   305  	nodeConfig.UpstreamConfig = params.UpstreamRPCConfig{
   306  		Enabled: true,
   307  		URL:     "https://mainnet.infura.io/v3/800c641949d64d768a5070a1b0511938",
   308  	}
   309  
   310  	nodeConfig.Name = "StatusIM"
   311  	clusterConfig, err := params.LoadClusterConfigFromFleet("status.prod")
   312  	if err != nil {
   313  		return nil, err
   314  	}
   315  	nodeConfig.ClusterConfig = *clusterConfig
   316  
   317  	nodeConfig.WalletConfig = params.WalletConfig{Enabled: true}
   318  	nodeConfig.LocalNotificationsConfig = params.LocalNotificationsConfig{Enabled: true}
   319  	nodeConfig.BrowsersConfig = params.BrowsersConfig{Enabled: true}
   320  	nodeConfig.PermissionsConfig = params.PermissionsConfig{Enabled: true}
   321  	nodeConfig.MailserversConfig = params.MailserversConfig{Enabled: true}
   322  	err = api.SetDefaultFleet(nodeConfig)
   323  	if err != nil {
   324  		return nil, err
   325  	}
   326  
   327  	nodeConfig.WakuV2Config = params.WakuV2Config{
   328  		Enabled:        true,
   329  		EnableDiscV5:   true,
   330  		DiscoveryLimit: 20,
   331  		UDPPort:        9002,
   332  	}
   333  
   334  	nodeConfig.ShhextConfig = params.ShhextConfig{
   335  		InstallationID:             installationID,
   336  		MaxMessageDeliveryAttempts: api.DefaultMaxMessageDeliveryAttempts,
   337  		MailServerConfirmations:    true,
   338  		VerifyTransactionURL:       "",
   339  		VerifyENSURL:               "",
   340  		VerifyENSContractAddress:   "",
   341  		VerifyTransactionChainID:   api.DefaultVerifyTransactionChainID,
   342  		DataSyncEnabled:            true,
   343  		PFSEnabled:                 true,
   344  	}
   345  
   346  	// TODO: check topics
   347  
   348  	return nodeConfig, nil
   349  }
   350  
   351  func ImportAccount(seedPhrase string, backend *api.GethStatusBackend) error {
   352  	backend.UpdateRootDataDir(*dataDir)
   353  	manager := backend.AccountManager()
   354  	if err := manager.InitKeystore(*dataDir); err != nil {
   355  		return err
   356  	}
   357  	err := backend.OpenAccounts()
   358  	if err != nil {
   359  		logger.Error("failed open accounts", err)
   360  		return err
   361  	}
   362  	generator := manager.AccountsGenerator()
   363  	generatedAccountInfo, err := generator.ImportMnemonic(seedPhrase, "")
   364  	if err != nil {
   365  		logger.Error("failed import mnemonic", err)
   366  		return err
   367  	}
   368  
   369  	derivedAddresses, err := generator.DeriveAddresses(generatedAccountInfo.ID, paths)
   370  	if err != nil {
   371  		logger.Error("failed derive", err)
   372  		return err
   373  	}
   374  
   375  	var exist bool
   376  	_, err = generator.StoreDerivedAccounts(generatedAccountInfo.ID, "", paths)
   377  	if err != nil && err.Error() == "account already exists" {
   378  		exist = true
   379  	} else if err != nil {
   380  		logger.Error("failed store derive", err)
   381  		return err
   382  	}
   383  
   384  	account := multiaccounts.Account{
   385  		KeyUID:        generatedAccountInfo.KeyUID,
   386  		KDFIterations: dbsetup.ReducedKDFIterationsNumber,
   387  	}
   388  	settings, err := defaultSettings(generatedAccountInfo, derivedAddresses, &seedPhrase)
   389  	if err != nil {
   390  		return err
   391  	}
   392  
   393  	nodeConfig, err := defaultNodeConfig(settings.InstallationID)
   394  	if err != nil {
   395  		return err
   396  	}
   397  
   398  	walletDerivedAccount := derivedAddresses[pathDefaultWallet]
   399  	walletAccount := &accounts.Account{
   400  		PublicKey: types.Hex2Bytes(walletDerivedAccount.PublicKey),
   401  		KeyUID:    generatedAccountInfo.KeyUID,
   402  		Address:   types.HexToAddress(walletDerivedAccount.Address),
   403  		ColorID:   "",
   404  		Wallet:    true,
   405  		Path:      pathDefaultWallet,
   406  		Name:      "Ethereum account",
   407  	}
   408  
   409  	chatDerivedAccount := derivedAddresses[pathDefaultChat]
   410  	chatAccount := &accounts.Account{
   411  		PublicKey: types.Hex2Bytes(chatDerivedAccount.PublicKey),
   412  		KeyUID:    generatedAccountInfo.KeyUID,
   413  		Address:   types.HexToAddress(chatDerivedAccount.Address),
   414  		Name:      settings.Name,
   415  		Chat:      true,
   416  		Path:      pathDefaultChat,
   417  	}
   418  
   419  	fmt.Println(nodeConfig)
   420  	accounts := []*accounts.Account{walletAccount, chatAccount}
   421  	if !exist {
   422  		return backend.StartNodeWithAccountAndInitialConfig(account, "", *settings, nodeConfig, accounts, nil)
   423  	}
   424  	return backend.StartNodeWithAccount(account, "", nodeConfig, nil)
   425  }
   426  
   427  func retrieveMessagesLoop(messenger *protocol.Messenger, tick time.Duration) {
   428  	ticker := time.NewTicker(tick)
   429  	defer ticker.Stop()
   430  
   431  	for { //nolint: gosimple
   432  		select {
   433  		case <-ticker.C:
   434  			_, err := messenger.RetrieveAll()
   435  			if err != nil {
   436  				logger.Error("failed to retrieve raw messages", "err", err)
   437  				continue
   438  			}
   439  		}
   440  	}
   441  }
   442  
   443  // exitOnInterruptSignal catches interrupt signal (SIGINT) and
   444  // stops the node. It times out after 5 seconds
   445  // if the node can not be stopped.
   446  func exitOnInterruptSignal(statusNode *node.StatusNode) <-chan struct{} {
   447  	interruptCh := make(chan struct{})
   448  	go func() {
   449  		sigChan := make(chan os.Signal, 1)
   450  		signal.Notify(sigChan, os.Interrupt)
   451  		defer signal.Stop(sigChan)
   452  		<-sigChan
   453  		close(interruptCh)
   454  		logger.Info("Got interrupt, shutting down...")
   455  		if err := statusNode.Stop(); err != nil {
   456  			logger.Error("Failed to stop node", "error", err)
   457  			os.Exit(1)
   458  		}
   459  	}()
   460  	return interruptCh
   461  }
   462  
   463  // startCollectingStats collects various stats about the node and other protocols like Whisper.
   464  func startNodeMetrics(interruptCh <-chan struct{}, statusNode *node.StatusNode) {
   465  	logger.Info("Starting collecting node metrics")
   466  
   467  	gNode := statusNode.GethNode()
   468  	if gNode == nil {
   469  		logger.Error("Failed to run metrics because it could not get the node")
   470  		return
   471  	}
   472  
   473  	ctx, cancel := context.WithCancel(context.Background())
   474  	defer cancel()
   475  	go func() {
   476  		// Try to subscribe and collect metrics. In case of an error, retry.
   477  		for {
   478  			if err := nodemetrics.SubscribeServerEvents(ctx, gNode); err != nil {
   479  				logger.Error("Failed to subscribe server events", "error", err)
   480  			} else {
   481  				// no error means that the subscription was terminated by purpose
   482  				return
   483  			}
   484  
   485  			time.Sleep(time.Second)
   486  		}
   487  	}()
   488  
   489  	<-interruptCh
   490  }