package main import ( "context" "encoding/json" "flag" "fmt" stdlog "log" "os" "path/filepath" "runtime" "strings" "time" "github.com/google/uuid" "golang.org/x/crypto/ssh/terminal" "github.com/ethereum/go-ethereum/log" "github.com/status-im/status-go/account/generator" "github.com/status-im/status-go/api" "github.com/status-im/status-go/eth-node/types" "github.com/status-im/status-go/multiaccounts" "github.com/status-im/status-go/multiaccounts/accounts" "github.com/status-im/status-go/multiaccounts/settings" "github.com/status-im/status-go/sqlite" "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/common" "github.com/status-im/status-go/protocol/identity/alias" "github.com/status-im/status-go/protocol/protobuf" wakuextn "github.com/status-im/status-go/services/wakuext" ) const ( serverClientName = "Statusd" ) var ( configFiles configFlags logLevel = flag.String("log", "", `Log level, one of: "ERROR", "WARN", "INFO", "DEBUG", and "TRACE"`) logWithoutColors = flag.Bool("log-without-color", false, "Disables log colors") ipcEnabled = flag.Bool("ipc", false, "Enable IPC RPC endpoint") ipcFile = flag.String("ipcfile", "", "Set IPC file path") seedPhrase = flag.String("seed-phrase", "", "Seed phrase") version = flag.Bool("version", false, "Print version and dump configuration") communityID = flag.String("community-id", "", "The id of the community") chatID = flag.String("chat-id", "", "The id of the chat") dataDir = flag.String("dir", getDefaultDataDir(), "Directory used by node to store data") networkID = flag.Int( "network-id", params.GoerliNetworkID, fmt.Sprintf( "A network ID: %d (Mainnet), %d (Goerli)", params.MainNetworkID, params.GoerliNetworkID, ), ) listenAddr = flag.String("addr", "", "address to bind listener to") ) // All general log messages in this package should be routed through this logger. var logger = log.New("package", "status-go/cmd/statusd") func init() { flag.Var(&configFiles, "c", "JSON configuration file(s). Multiple configuration files can be specified, and will be merged in occurrence order") } // nolint:gocyclo func main() { colors := terminal.IsTerminal(int(os.Stdin.Fd())) if err := logutils.OverrideRootLog(true, "ERROR", logutils.FileOptions{}, colors); err != nil { stdlog.Fatalf("Error initializing logger: %v", err) } flag.Usage = printUsage flag.Parse() if flag.NArg() > 0 { printUsage() logger.Error("Extra args in command line: %v", flag.Args()) os.Exit(1) } opts := []params.Option{} config, err := params.NewNodeConfigWithDefaultsAndFiles( *dataDir, uint64(*networkID), opts, configFiles, ) if err != nil { printUsage() logger.Error(err.Error()) os.Exit(1) } // Use listenAddr if and only if explicitly provided in the arguments. // The default value is set in params.NewNodeConfigWithDefaultsAndFiles(). if *listenAddr != "" { config.ListenAddr = *listenAddr } // enable IPC RPC if *ipcEnabled { config.IPCEnabled = true config.IPCFile = *ipcFile } // set up logging options setupLogging(config) // We want statusd to be distinct from StatusIM client. config.Name = serverClientName if *version { printVersion(config) return } backend := api.NewGethStatusBackend() err = ImportAccount(*seedPhrase, backend) if err != nil { logger.Error("failed import account", "err", err) return } wakuextservice := backend.StatusNode().WakuExtService() if wakuextservice == nil { logger.Error("wakuext not available") return } wakuext := wakuextn.NewPublicAPI(wakuextservice) // This will start the push notification server as well as // the config is set to Enabled _, err = wakuext.StartMessenger() if err != nil { logger.Error("failed to start messenger", "error", err) return } messenger := wakuextservice.Messenger() community, err := messenger.RequestCommunityInfoFromMailserver(*communityID, true) if err != nil { logger.Error("community error", "error", err) return } chat := community.Chats()[*chatID] if chat == nil { logger.Warn("Chat not found") return } logger.Info("GOT community", "comm", chat) response, err := messenger.JoinCommunity(context.Background(), community.ID(), false) if err != nil { logger.Error("failed to join community", "err", err) } var targetChat *protocol.Chat for _, c := range response.Chats() { if strings.Contains(c.ID, *chatID) { targetChat = c } } if targetChat == nil { logger.Warn("chat not found") return } id := uuid.New().String() ticker := time.NewTicker(2 * time.Second) count := 0 for { // nolint: gosimple select { case <-ticker.C: count++ timestamp := time.Now().Format(time.RFC3339) logger.Info("Publishing", "id", id, "count", count, "time", timestamp) inputMessage := &common.Message{} inputMessage.Text = fmt.Sprintf("%d\n%s\n%s", count, timestamp, id) inputMessage.LocalChatID = targetChat.ID inputMessage.ChatId = targetChat.ID inputMessage.ContentType = protobuf.ChatMessage_TEXT_PLAIN _, err := messenger.SendChatMessage(context.Background(), inputMessage) if err != nil { logger.Error("failed to send a message", "err", err) } } } } func getDefaultDataDir() string { if home := os.Getenv("HOME"); home != "" { return filepath.Join(home, ".statusd") } return "./statusd-data" } func setupLogging(config *params.NodeConfig) { if *logLevel != "" { config.LogLevel = *logLevel } logSettings := logutils.LogSettings{ Enabled: config.LogEnabled, MobileSystem: config.LogMobileSystem, Level: config.LogLevel, File: config.LogFile, MaxSize: config.LogMaxSize, MaxBackups: config.LogMaxBackups, CompressRotated: config.LogCompressRotated, } colors := !(*logWithoutColors) && terminal.IsTerminal(int(os.Stdin.Fd())) if err := logutils.OverrideRootLogWithConfig(logSettings, colors); err != nil { stdlog.Fatalf("Error initializing logger: %v", err) } } // printVersion prints verbose output about version and config. func printVersion(config *params.NodeConfig) { fmt.Println(strings.Title(config.Name)) fmt.Println("Version:", config.Version) 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()) fmt.Println("Loaded Config: ", config) } func printUsage() { usage := ` Usage: ping-community [options] Example: ping-community --seed-phrase "your seed phrase" --community-id "community-id" --chat-id "chat-id" Options: ` fmt.Fprint(os.Stderr, usage) flag.PrintDefaults() } const pathWalletRoot = "m/44'/60'/0'/0" const pathEIP1581 = "m/43'/60'/1581'" const pathDefaultChat = pathEIP1581 + "/0'/0" const pathDefaultWallet = pathWalletRoot + "/0" var paths = []string{pathWalletRoot, pathEIP1581, pathDefaultChat, pathDefaultWallet} func defaultSettings(generatedAccountInfo generator.GeneratedAccountInfo, derivedAddresses map[string]generator.AccountInfo, mnemonic *string) (*settings.Settings, error) { chatKeyString := derivedAddresses[pathDefaultChat].PublicKey settings := &settings.Settings{} settings.KeyUID = generatedAccountInfo.KeyUID settings.Address = types.HexToAddress(generatedAccountInfo.Address) settings.WalletRootAddress = types.HexToAddress(derivedAddresses[pathWalletRoot].Address) // Set chat key & name name, err := alias.GenerateFromPublicKeyString(chatKeyString) if err != nil { return nil, err } settings.Name = name settings.PublicKey = chatKeyString settings.DappsAddress = types.HexToAddress(derivedAddresses[pathDefaultWallet].Address) settings.EIP1581Address = types.HexToAddress(derivedAddresses[pathEIP1581].Address) settings.Mnemonic = mnemonic signingPhrase, err := buildSigningPhrase() if err != nil { return nil, err } settings.SigningPhrase = signingPhrase settings.SendPushNotifications = true settings.InstallationID = uuid.New().String() settings.UseMailservers = true settings.PreviewPrivacy = true settings.Currency = "usd" settings.ProfilePicturesVisibility = 1 settings.LinkPreviewRequestEnabled = true visibleTokens := make(map[string][]string) visibleTokens["mainnet"] = []string{"SNT"} visibleTokensJSON, err := json.Marshal(visibleTokens) if err != nil { return nil, err } visibleTokenJSONRaw := json.RawMessage(visibleTokensJSON) settings.WalletVisibleTokens = &visibleTokenJSONRaw // TODO: fix this networks := make([]map[string]string, 0) networksJSON, err := json.Marshal(networks) if err != nil { return nil, err } networkRawMessage := json.RawMessage(networksJSON) settings.Networks = &networkRawMessage settings.CurrentNetwork = "mainnet_rpc" return settings, nil } func defaultNodeConfig(installationID string) (*params.NodeConfig, error) { // Set mainnet nodeConfig := ¶ms.NodeConfig{} nodeConfig.NetworkID = 1 nodeConfig.LogLevel = "ERROR" nodeConfig.DataDir = "/ethereum/mainnet_rpc" nodeConfig.UpstreamConfig = params.UpstreamRPCConfig{ Enabled: true, URL: "https://mainnet.infura.io/v3/800c641949d64d768a5070a1b0511938", } nodeConfig.Name = "StatusIM" clusterConfig, err := params.LoadClusterConfigFromFleet("eth.prod") if err != nil { return nil, err } nodeConfig.ClusterConfig = *clusterConfig nodeConfig.WalletConfig = params.WalletConfig{Enabled: true} nodeConfig.LocalNotificationsConfig = params.LocalNotificationsConfig{Enabled: true} nodeConfig.BrowsersConfig = params.BrowsersConfig{Enabled: true} nodeConfig.PermissionsConfig = params.PermissionsConfig{Enabled: true} nodeConfig.MailserversConfig = params.MailserversConfig{Enabled: true} nodeConfig.EnableNTPSync = true nodeConfig.WakuConfig = params.WakuConfig{ Enabled: true, LightClient: true, MinimumPoW: 0.000001, } nodeConfig.ShhextConfig = params.ShhextConfig{ BackupDisabledDataDir: "", InstallationID: installationID, MaxMessageDeliveryAttempts: 6, MailServerConfirmations: true, VerifyTransactionURL: "", VerifyENSURL: "", VerifyENSContractAddress: "", VerifyTransactionChainID: 1, DataSyncEnabled: true, PFSEnabled: true, } // TODO: check topics return nodeConfig, nil } func ImportAccount(seedPhrase string, backend *api.GethStatusBackend) error { backend.UpdateRootDataDir("./tmp") manager := backend.AccountManager() if err := manager.InitKeystore("./tmp"); err != nil { return err } err := backend.OpenAccounts() if err != nil { logger.Error("failed open accounts", err) return err } generator := manager.AccountsGenerator() generatedAccountInfo, err := generator.ImportMnemonic(seedPhrase, "") if err != nil { return err } derivedAddresses, err := generator.DeriveAddresses(generatedAccountInfo.ID, paths) if err != nil { return err } _, err = generator.StoreDerivedAccounts(generatedAccountInfo.ID, "", paths) if err != nil { return err } account := multiaccounts.Account{ KeyUID: generatedAccountInfo.KeyUID, KDFIterations: sqlite.ReducedKDFIterationsNumber, } settings, err := defaultSettings(generatedAccountInfo, derivedAddresses, &seedPhrase) if err != nil { return err } nodeConfig, err := defaultNodeConfig(settings.InstallationID) if err != nil { return err } walletDerivedAccount := derivedAddresses[pathDefaultWallet] walletAccount := &accounts.Account{ PublicKey: types.Hex2Bytes(walletDerivedAccount.PublicKey), KeyUID: generatedAccountInfo.KeyUID, Address: types.HexToAddress(walletDerivedAccount.Address), Color: "", Wallet: true, Path: pathDefaultWallet, Name: "Ethereum account", } chatDerivedAccount := derivedAddresses[pathDefaultChat] chatAccount := &accounts.Account{ PublicKey: types.Hex2Bytes(chatDerivedAccount.PublicKey), KeyUID: generatedAccountInfo.KeyUID, Address: types.HexToAddress(chatDerivedAccount.Address), Name: settings.Name, Chat: true, Path: pathDefaultChat, } fmt.Println(nodeConfig) accounts := []*accounts.Account{walletAccount, chatAccount} err = backend.StartNodeWithAccountAndInitialConfig(account, "", *settings, nodeConfig, accounts) if err != nil { logger.Error("start node", err) return err } return nil }