diff --git a/.gitignore b/.gitignore index ed8a3eb77..eb792eca7 100644 --- a/.gitignore +++ b/.gitignore @@ -92,3 +92,9 @@ test_*.log vendor/github.com/waku-org/go-zerokit-rln-x86_64/ vendor/github.com/waku-org/go-zerokit-rln-apple/ vendor/github.com/waku-org/go-zerokit-rln-arm/ + +# status-cli logs +alice.log +bob.log +test-alice/ +test-bob/ \ No newline at end of file diff --git a/Makefile b/Makefile index ff9aa4887..260ad2cb7 100644 --- a/Makefile +++ b/Makefile @@ -111,6 +111,9 @@ statusd-prune: build/bin/statusd-prune spiff-workflow: ##@build Build node for SpiffWorkflow BPMN software spiff-workflow: build/bin/spiff-workflow +status-cli: ##@build Build status-cli to send messages +status-cli: build/bin/status-cli + statusd-prune-docker-image: ##@statusd-prune Build statusd-prune docker image @echo "Building docker image for ststusd-prune..." docker build --file _assets/build/Dockerfile-prune . \ diff --git a/api/defaults.go b/api/defaults.go index 9f5b60126..1b6f81a6c 100644 --- a/api/defaults.go +++ b/api/defaults.go @@ -186,7 +186,7 @@ func buildWalletConfig(request *requests.WalletSecretsConfig) params.WalletConfi return walletConfig } -func defaultNodeConfig(installationID string, request *requests.CreateAccount) (*params.NodeConfig, error) { +func defaultNodeConfig(installationID string, request *requests.CreateAccount, opts ...params.Option) (*params.NodeConfig, error) { // Set mainnet nodeConfig := ¶ms.NodeConfig{} nodeConfig.NetworkID = request.NetworkID @@ -272,6 +272,12 @@ func defaultNodeConfig(installationID string, request *requests.CreateAccount) ( nodeConfig.Networks = BuildDefaultNetworks(request) + for _, opt := range opts { + if err := opt(nodeConfig); err != nil { + return nil, err + } + } + return nodeConfig, nil } diff --git a/api/geth_backend.go b/api/geth_backend.go index d3c4bfd48..994515986 100644 --- a/api/geth_backend.go +++ b/api/geth_backend.go @@ -1298,7 +1298,7 @@ func (b *GethStatusBackend) GetKeyUIDByMnemonic(mnemonic string) (string, error) return info.KeyUID, nil } -func (b *GethStatusBackend) generateOrImportAccount(mnemonic string, customizationColorClock uint64, request *requests.CreateAccount) (*multiaccounts.Account, error) { +func (b *GethStatusBackend) generateOrImportAccount(mnemonic string, customizationColorClock uint64, request *requests.CreateAccount, opts ...params.Option) (*multiaccounts.Account, error) { keystoreDir := keystoreRelativePath b.UpdateRootDataDir(request.BackupDisabledDataDir) @@ -1382,7 +1382,7 @@ func (b *GethStatusBackend) generateOrImportAccount(mnemonic string, customizati //settings.MnemonicWasNotShown = true } - nodeConfig, err := defaultNodeConfig(settings.InstallationID, request) + nodeConfig, err := defaultNodeConfig(settings.InstallationID, request, opts...) if err != nil { return nil, err } @@ -1430,12 +1430,12 @@ func (b *GethStatusBackend) generateOrImportAccount(mnemonic string, customizati return &account, nil } -func (b *GethStatusBackend) CreateAccountAndLogin(request *requests.CreateAccount) (*multiaccounts.Account, error) { +func (b *GethStatusBackend) CreateAccountAndLogin(request *requests.CreateAccount, opts ...params.Option) (*multiaccounts.Account, error) { if err := request.Validate(); err != nil { return nil, err } - return b.generateOrImportAccount("", 1, request) + return b.generateOrImportAccount("", 1, request, opts...) } func (b *GethStatusBackend) ConvertToRegularAccount(mnemonic string, currPassword string, newPassword string) error { diff --git a/cmd/status-cli/.gitignore b/cmd/status-cli/.gitignore new file mode 100644 index 000000000..9d79faeca --- /dev/null +++ b/cmd/status-cli/.gitignore @@ -0,0 +1,3 @@ +log +*.log +status-cli \ No newline at end of file diff --git a/cmd/status-cli/README.md b/cmd/status-cli/README.md new file mode 100644 index 000000000..de73f49d5 --- /dev/null +++ b/cmd/status-cli/README.md @@ -0,0 +1,70 @@ +# Status CLI + +The Status CLI is a command line interface for interacting with the Status messenging process. It is a tool for developers and QAs to test the communication workflow without running Status desktop and mobile app. + +## Features + +- Create a new account +- Send and receive contact request +- DM between contacts + +## Build + +Go to `cmd/status-cli` directory and build the binary + +```bash +go build +``` + +You can also run `make status-cli` in the root directory to build the binary. + +## Run + +### Run `serve` command: + +```bash +# run alice's server +./status-cli serve + +# run Bob's server in another terminal with the logged pubkey of Alice +./status-cli serve -n bob -p 8565 -a +``` + +You can send direct messages through terminal or JSON RPC. + +JSON RPC examples: + +```bash +# get waku info +curl -XPOST http://127.0.0.1:8545 -H 'Content-type: application/json' -d '{"jsonrpc":"2.0","method":"waku_info","params":[],"id":1}' + +# send contact request from bob to alice (use -a flag will automatacally send contact request when starting) +curl -XPOST http://127.0.0.1:8565 -H 'Content-type: application/json' -d '{"jsonrpc":"2.0","method":"wakuext_sendContactRequest","params":[{"id": "0x0436470da23039f10c1588bc6b9fcbd4b815bf9fae4dc09c0fb05a7eaaf1670b5dbdbc757630d54bf2f8be45a796304dc42506c3f4172f499f610a9ed85d9b0d4c", "message": "hello"}],"id":1}' + +# send dm from bob to alice +curl -XPOST http://127.0.0.1:8565 -H 'Content-type: application/json' -d '{"jsonrpc":"2.0","method":"wakuext_sendOneToOneMessage","params":[{"id": "0x0436470da23039f10c1588bc6b9fcbd4b815bf9fae4dc09c0fb05a7eaaf1670b5dbdbc757630d54bf2f8be45a796304dc42506c3f4172f499f610a9ed85d9b0d4c", "message": "how are you"}],"id":1}' + +# send dm from alice to bob +curl -XPOST http://127.0.0.1:8545 -H 'Content-type: application/json' -d '{"jsonrpc":"2.0","method":"wakuext_sendOneToOneMessage","params":[{"id": "0x042c0ce856c41ad6d3f651a84c83f646cdafdf3a26a3d69bce3a6ccf59b23b5a366c12162045d5066abad7912741a6e6c6e8e11e7826c4c850a1de7a2bae24a79c", "message": "Im fine, and you?"}],"id":1}' +``` + + +### Run `dm` command: + +```bash +# simulate DM between two accounts +./status-cli dm + +# simulate DM in a interactive way +./status-cli dm -i + +# simulate DM with 3 messages +./status-cli dm -c 3 + +# run in light mode +./status-cli dm --light +``` + +You can run the commands with `--light` to work as a light client. + +Logs are recorded in file `*.log` and terminal. \ No newline at end of file diff --git a/cmd/status-cli/main.go b/cmd/status-cli/main.go new file mode 100644 index 000000000..5d08d7ebf --- /dev/null +++ b/cmd/status-cli/main.go @@ -0,0 +1,104 @@ +package main + +import ( + "os" + "time" + + "go.uber.org/zap" + + "github.com/status-im/status-go/api" + "github.com/status-im/status-go/protocol" + + "github.com/urfave/cli/v2" +) + +const LightFlag = "light" +const InteractiveFlag = "interactive" +const CountFlag = "count" +const NameFlag = "name" +const AddFlag = "add" +const PortFlag = "port" + +const RetrieveInterval = 300 * time.Millisecond +const SendInterval = 1 * time.Second +const WaitingInterval = 5 * time.Second + +var CommonFlags = []cli.Flag{ + &cli.BoolFlag{ + Name: LightFlag, + Aliases: []string{"l"}, + Usage: "Enable light mode", + }, +} + +var DmFlags = append([]cli.Flag{ + &cli.BoolFlag{ + Name: InteractiveFlag, + Aliases: []string{"i"}, + Usage: "Use interactive mode", + }, + &cli.IntFlag{ + Name: CountFlag, + Aliases: []string{"c"}, + Value: 1, + Usage: "How many messages to sent from each user", + }, +}, CommonFlags...) + +var ServeFlags = append([]cli.Flag{ + &cli.StringFlag{ + Name: NameFlag, + Aliases: []string{"n"}, + Value: "Alice", + Usage: "Name of the user", + }, + &cli.StringFlag{ + Name: AddFlag, + Aliases: []string{"a"}, + Usage: "Add a friend with the public key", + }, + &cli.IntFlag{ + Name: PortFlag, + Aliases: []string{"p"}, + Value: 8545, + Usage: "HTTP Server port to listen on", + }, +}, CommonFlags...) + +var logger *zap.SugaredLogger + +type StatusCLI struct { + name string + messenger *protocol.Messenger + backend *api.GethStatusBackend + logger *zap.SugaredLogger +} + +func main() { + app := &cli.App{ + Commands: []*cli.Command{ + { + Name: "dm", + Aliases: []string{"d"}, + Usage: "Send direct message", + Flags: DmFlags, + Action: func(cCtx *cli.Context) error { + return simulate(cCtx) + }, + }, + { + Name: "serve", + Aliases: []string{"s"}, + Usage: "Start a server to send and receive messages", + Flags: ServeFlags, + Action: func(cCtx *cli.Context) error { + return serve(cCtx) + }, + }, + }, + } + + if err := app.Run(os.Args); err != nil { + logger.Fatal(err) + } +} diff --git a/cmd/status-cli/message.go b/cmd/status-cli/message.go new file mode 100644 index 000000000..75f83d3ea --- /dev/null +++ b/cmd/status-cli/message.go @@ -0,0 +1,144 @@ +package main + +import ( + "bufio" + "context" + "os" + "strings" + "sync" + "time" + + "github.com/status-im/status-go/eth-node/types" + "github.com/status-im/status-go/protocol/common" + "github.com/status-im/status-go/protocol/protobuf" + "github.com/status-im/status-go/protocol/requests" + + "github.com/urfave/cli/v2" +) + +func sendContactRequest(cCtx *cli.Context, from *StatusCLI, toID string) error { + from.logger.Info("send contact request, contact public key: ", toID) + request := &requests.SendContactRequest{ + ID: toID, + Message: "Hello!", + } + resp, err := from.messenger.SendContactRequest(cCtx.Context, request) + from.logger.Info("function SendContactRequest response.messages: ", resp.Messages()) + if err != nil { + return err + } + + return nil +} + +func sendContactRequestAcceptance(cCtx *cli.Context, from *StatusCLI, msgID string) error { + from.logger.Info("accept contact request, message ID: ", msgID) + resp, err := from.messenger.AcceptContactRequest(cCtx.Context, &requests.AcceptContactRequest{ID: types.Hex2Bytes(msgID)}) + if err != nil { + return err + } + from.logger.Info("function AcceptContactRequest response: ", resp.Messages()) + + return nil +} + +func sendDirectMessage(ctx context.Context, from *StatusCLI, text string) error { + if len(from.messenger.MutualContacts()) == 0 { + return nil + } + chat := from.messenger.Chat(from.messenger.MutualContacts()[0].ID) + from.logger.Info("send message to contact: ", chat.ID) + + clock, timestamp := chat.NextClockAndTimestamp(from.messenger.GetTransport()) + inputMessage := common.NewMessage() + inputMessage.ChatId = chat.ID + inputMessage.LocalChatID = chat.ID + inputMessage.Clock = clock + inputMessage.Timestamp = timestamp + inputMessage.MessageType = protobuf.MessageType_ONE_TO_ONE + inputMessage.ContentType = protobuf.ChatMessage_TEXT_PLAIN + inputMessage.Text = text + + _, err := from.messenger.SendChatMessage(ctx, inputMessage) + if err != nil { + return err + } + + return nil +} + +func retrieveMessagesLoop(ctx context.Context, cli *StatusCLI, tick time.Duration, msgCh chan string, wg *sync.WaitGroup) { + defer wg.Done() + + ticker := time.NewTicker(tick) + defer ticker.Stop() + + cli.logger.Info("retrieve messages...") + + for { + select { + case <-ticker.C: + response, err := cli.messenger.RetrieveAll() + if err != nil { + cli.logger.Error("failed to retrieve raw messages", "err", err) + continue + } + if response != nil && len(response.Messages()) != 0 { + for _, message := range response.Messages() { + cli.logger.Info("receive message: ", message.Text) + if message.ContentType == protobuf.ChatMessage_SYSTEM_MESSAGE_MUTUAL_EVENT_SENT { + msgCh <- message.ID + } + } + } + case <-ctx.Done(): + return + } + } +} + +func sendMessageLoop(ctx context.Context, cli *StatusCLI, tick time.Duration, wg *sync.WaitGroup, sem chan struct{}, cancel context.CancelFunc) { + defer wg.Done() + + ticker := time.NewTicker(tick) + defer ticker.Stop() + + reader := bufio.NewReader(os.Stdin) + for { + select { + case <-ticker.C: + if len(cli.messenger.MutualContacts()) == 0 { + continue + } + sem <- struct{}{} + cli.logger.Info("Enter your message to send: (type 'quit' or 'q' to exit)") + message, err := reader.ReadString('\n') + if err != nil { + <-sem + cli.logger.Error("failed to read input", err) + continue + } + + message = strings.TrimSpace(message) + if message == "quit" || message == "q" || strings.Contains(message, "\x03") { + cancel() + <-sem + return + } + if message == "" { + <-sem + continue + } + + err = sendDirectMessage(ctx, cli, message) + time.Sleep(WaitingInterval) + <-sem + if err != nil { + cli.logger.Error("failed to send direct message: ", err) + continue + } + case <-ctx.Done(): + return + } + } +} diff --git a/cmd/status-cli/serve.go b/cmd/status-cli/serve.go new file mode 100644 index 000000000..8abe1b0e7 --- /dev/null +++ b/cmd/status-cli/serve.go @@ -0,0 +1,79 @@ +package main + +import ( + "context" + "log" + "os" + "os/signal" + "sync" + "syscall" + + "github.com/urfave/cli/v2" + "go.uber.org/zap" +) + +func serve(cCtx *cli.Context) error { + ctx, cancel := context.WithCancel(cCtx.Context) + + go func() { + sig := make(chan os.Signal, 1) + signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) + <-sig + cancel() + }() + + rawLogger, err := zap.NewDevelopment() + if err != nil { + log.Fatalf("Error initializing logger: %v", err) + } + logger = rawLogger.Sugar() + + logger.Info("Running serve command, flags passed:") + for _, flag := range ServeFlags { + logger.Infof("-%s %v", flag.Names()[0], cCtx.Value(flag.Names()[0])) + } + + name := cCtx.String(NameFlag) + port := cCtx.Int(PortFlag) + + messenger, err := startMessenger(cCtx, name, port) + if err != nil { + return err + } + defer stopMessenger(messenger) + + // Retrieve for messages + var wg sync.WaitGroup + msgCh := make(chan string) + + wg.Add(1) + go retrieveMessagesLoop(ctx, messenger, RetrieveInterval, msgCh, &wg) + + // Send contact request from Alice to Bob, bob accept the request + dest := cCtx.String(AddFlag) + if dest != "" { + err := sendContactRequest(cCtx, messenger, dest) + if err != nil { + return err + } + } + + go func() { + msgID := <-msgCh + err = sendContactRequestAcceptance(cCtx, messenger, msgID) + if err != nil { + logger.Error(err) + return + } + }() + + // Send message if mutual contact exists + sem := make(chan struct{}, 1) + wg.Add(1) + go sendMessageLoop(ctx, messenger, SendInterval, &wg, sem, cancel) + + wg.Wait() + logger.Info("Exiting") + + return nil +} diff --git a/cmd/status-cli/simulate.go b/cmd/status-cli/simulate.go new file mode 100644 index 000000000..27c76ecef --- /dev/null +++ b/cmd/status-cli/simulate.go @@ -0,0 +1,106 @@ +package main + +import ( + "context" + "log" + "os" + "os/signal" + "sync" + "syscall" + "time" + + "github.com/ethereum/go-ethereum/crypto" + "github.com/status-im/status-go/eth-node/types" + + "github.com/urfave/cli/v2" + "go.uber.org/zap" +) + +func simulate(cCtx *cli.Context) error { + ctx, cancel := context.WithCancel(cCtx.Context) + + go func() { + sig := make(chan os.Signal, 1) + signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) + <-sig + cancel() + }() + + rawLogger, err := zap.NewDevelopment() + if err != nil { + log.Fatalf("Error initializing logger: %v", err) + } + logger = rawLogger.Sugar() + + logger.Info("Running dm command, flags passed:") + for _, flag := range DmFlags { + logger.Infof("-%s %v", flag.Names()[0], cCtx.Value(flag.Names()[0])) + } + + // Start Alice and Bob's messengers + alice, err := startMessenger(cCtx, "Alice", 0) + if err != nil { + return err + } + defer stopMessenger(alice) + + bob, err := startMessenger(cCtx, "Bob", 0) + if err != nil { + return err + } + defer stopMessenger(bob) + + // Retrieve for messages + msgCh := make(chan string) + var wg sync.WaitGroup + + wg.Add(1) + go retrieveMessagesLoop(ctx, alice, RetrieveInterval, nil, &wg) + wg.Add(1) + go retrieveMessagesLoop(ctx, bob, RetrieveInterval, msgCh, &wg) + + // Send contact request from Alice to Bob, bob accept the request + time.Sleep(WaitingInterval) + destID := types.EncodeHex(crypto.FromECDSAPub(bob.messenger.IdentityPublicKey())) + err = sendContactRequest(cCtx, alice, destID) + if err != nil { + return err + } + + msgID := <-msgCh + err = sendContactRequestAcceptance(cCtx, bob, msgID) + if err != nil { + return err + } + + // Send DM between alice to bob + interactive := cCtx.Bool(InteractiveFlag) + if interactive { + sem := make(chan struct{}, 1) + wg.Add(1) + go sendMessageLoop(ctx, alice, SendInterval, &wg, sem, cancel) + wg.Add(1) + go sendMessageLoop(ctx, bob, SendInterval, &wg, sem, cancel) + } else { + time.Sleep(WaitingInterval) + for i := 0; i < cCtx.Int(CountFlag); i++ { + err = sendDirectMessage(ctx, alice, "hello bob :)") + if err != nil { + return err + } + time.Sleep(WaitingInterval) + + err = sendDirectMessage(ctx, bob, "hello Alice ~") + if err != nil { + return err + } + time.Sleep(WaitingInterval) + } + cancel() + } + + wg.Wait() + logger.Info("Exiting") + + return nil +} diff --git a/cmd/status-cli/util.go b/cmd/status-cli/util.go new file mode 100644 index 000000000..d0767ccb9 --- /dev/null +++ b/cmd/status-cli/util.go @@ -0,0 +1,111 @@ +package main + +import ( + "errors" + "fmt" + "os" + "strings" + "time" + + "github.com/status-im/status-go/api" + "github.com/status-im/status-go/logutils" + "github.com/status-im/status-go/params" + "github.com/status-im/status-go/protocol/requests" + "github.com/status-im/status-go/services/wakuv2ext" + + "github.com/urfave/cli/v2" + "go.uber.org/zap" +) + +func setupLogger(file string) *zap.Logger { + logFile := fmt.Sprintf("%s.log", strings.ToLower(file)) + logSettings := logutils.LogSettings{ + Enabled: true, + MobileSystem: false, + Level: "DEBUG", + File: logFile, + MaxSize: 100, + MaxBackups: 3, + CompressRotated: true, + } + if err := logutils.OverrideRootLogWithConfig(logSettings, false); err != nil { + logger.Fatalf("Error initializing logger: %v", err) + } + + newLogger := logutils.ZapLogger() + + return newLogger +} + +func withHTTP(port int) params.Option { + return func(c *params.NodeConfig) error { + c.APIModules = "waku,wakuext,wakuv2,permissions,eth" + c.HTTPEnabled = true + c.HTTPHost = "127.0.0.1" + c.HTTPPort = port + + return nil + } +} + +func startMessenger(cCtx *cli.Context, name string, port int) (*StatusCLI, error) { + namedLogger := logger.Named(name) + namedLogger.Info("starting messager") + + _ = setupLogger(name) + + path := fmt.Sprintf("./test-%s", strings.ToLower(name)) + err := os.MkdirAll(path, os.ModePerm) + if err != nil { + return nil, err + } + + backend := api.NewGethStatusBackend() + + createAccountRequest := &requests.CreateAccount{ + DisplayName: "some-display-name", + CustomizationColor: "#ffffff", + Emoji: "some", + Password: "some-password", + BackupDisabledDataDir: fmt.Sprintf("./test-%s", strings.ToLower(name)), + NetworkID: 1, + LogFilePath: "log", + } + opts := []params.Option{withHTTP(port)} + _, err = backend.CreateAccountAndLogin(createAccountRequest, opts...) + if err != nil { + return nil, err + } + + wakuService := backend.StatusNode().WakuV2ExtService() + if wakuService == nil { + return nil, errors.New("waku service is not available") + } + wakuAPI := wakuv2ext.NewPublicAPI(wakuService) + + messenger := wakuAPI.Messenger() + _, err = wakuAPI.StartMessenger() + if err != nil { + return nil, err + } + + namedLogger.Info("messenger started, public key: ", messenger.IdentityPublicKeyString()) + + time.Sleep(WaitingInterval) + + data := StatusCLI{ + name: name, + messenger: messenger, + backend: backend, + logger: namedLogger, + } + + return &data, nil +} + +func stopMessenger(cli *StatusCLI) { + err := cli.backend.StopNode() + if err != nil { + logger.Error(err) + } +} diff --git a/protocol/messenger.go b/protocol/messenger.go index b7a72a83c..a65fd6564 100644 --- a/protocol/messenger.go +++ b/protocol/messenger.go @@ -3489,6 +3489,10 @@ func (m *Messenger) GetStats() types.StatsSummary { return m.transport.GetStats() } +func (m *Messenger) GetTransport() *transport.Transport { + return m.transport +} + type CurrentMessageState struct { // Message is the protobuf message received Message *protobuf.ChatMessage diff --git a/transactions/pendingtxtracker.go b/transactions/pendingtxtracker.go index 903c9d59a..d5340fac2 100644 --- a/transactions/pendingtxtracker.go +++ b/transactions/pendingtxtracker.go @@ -470,6 +470,9 @@ func rowsToTransactions(rows *sql.Rows) (transactions []*PendingTransaction, err } func (tm *PendingTxTracker) GetAllPending() ([]*PendingTransaction, error) { + if tm.db == nil { + return nil, errors.New("database is not initialized") + } rows, err := tm.db.Query(selectFromPending+"WHERE status = ?", Pending) if err != nil { return nil, err