code.vegaprotocol.io/vega@v0.79.0/cmd/vegawallet/commands/service_run.go (about)

     1  // Copyright (C) 2023 Gobalsky Labs Limited
     2  //
     3  // This program is free software: you can redistribute it and/or modify
     4  // it under the terms of the GNU Affero General Public License as
     5  // published by the Free Software Foundation, either version 3 of the
     6  // License, or (at your option) any later version.
     7  //
     8  // This program is distributed in the hope that it will be useful,
     9  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    10  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    11  // GNU Affero General Public License for more details.
    12  //
    13  // You should have received a copy of the GNU Affero General Public License
    14  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    15  
    16  package cmd
    17  
    18  import (
    19  	"context"
    20  	"errors"
    21  	"fmt"
    22  	"io"
    23  	"os"
    24  	"os/signal"
    25  	"strings"
    26  	"sync/atomic"
    27  	"syscall"
    28  	"time"
    29  
    30  	"code.vegaprotocol.io/vega/cmd/vegawallet/commands/cli"
    31  	"code.vegaprotocol.io/vega/cmd/vegawallet/commands/flags"
    32  	"code.vegaprotocol.io/vega/cmd/vegawallet/commands/printer"
    33  	vgclose "code.vegaprotocol.io/vega/libs/close"
    34  	vgjob "code.vegaprotocol.io/vega/libs/job"
    35  	vgterm "code.vegaprotocol.io/vega/libs/term"
    36  	vgzap "code.vegaprotocol.io/vega/libs/zap"
    37  	"code.vegaprotocol.io/vega/paths"
    38  	coreversion "code.vegaprotocol.io/vega/version"
    39  	walletapi "code.vegaprotocol.io/vega/wallet/api"
    40  	"code.vegaprotocol.io/vega/wallet/api/interactor"
    41  	netStoreV1 "code.vegaprotocol.io/vega/wallet/network/store/v1"
    42  	"code.vegaprotocol.io/vega/wallet/preferences"
    43  	"code.vegaprotocol.io/vega/wallet/service"
    44  	svcStoreV1 "code.vegaprotocol.io/vega/wallet/service/store/v1"
    45  	serviceV1 "code.vegaprotocol.io/vega/wallet/service/v1"
    46  	serviceV2 "code.vegaprotocol.io/vega/wallet/service/v2"
    47  	"code.vegaprotocol.io/vega/wallet/service/v2/connections"
    48  	tokenStoreV1 "code.vegaprotocol.io/vega/wallet/service/v2/connections/store/longliving/v1"
    49  	sessionStoreV1 "code.vegaprotocol.io/vega/wallet/service/v2/connections/store/session/v1"
    50  	"code.vegaprotocol.io/vega/wallet/version"
    51  	"code.vegaprotocol.io/vega/wallet/wallets"
    52  
    53  	"github.com/golang/protobuf/jsonpb"
    54  	"github.com/muesli/cancelreader"
    55  	"github.com/spf13/cobra"
    56  	"go.uber.org/zap"
    57  	"golang.org/x/term"
    58  )
    59  
    60  const MaxConsentRequests = 100
    61  
    62  var (
    63  	ErrEnableAutomaticConsentFlagIsRequiredWithoutTTY = errors.New("--automatic-consent flag is required without TTY")
    64  	ErrMsysUnsupported                                = errors.New("this command is not supported on msys, please use a standard windows terminal")
    65  )
    66  
    67  var (
    68  	runServiceLong = cli.LongDesc(`
    69  		Start a Vega wallet service behind an HTTP server.
    70  
    71  		By default, every incoming transactions will have to be reviewed in the
    72  		terminal.
    73  
    74  		To terminate the service, hit ctrl+c.
    75  
    76  		NOTE: The --output flag is ignored in this command.
    77  
    78  		WARNING: This command is not supported on msys, due to some system
    79          incompatibilities with the user input management.
    80  		Non-exhaustive list of affected systems: Cygwin, minty, git-bash.
    81  	`)
    82  
    83  	runServiceExample = cli.Examples(`
    84  		# Start the service
    85  		{{.Software}} service run --network NETWORK
    86  
    87  		# Start the service with automatic consent of incoming transactions
    88  		{{.Software}} service run --network NETWORK --automatic-consent
    89  
    90  		# Start the service without verifying network version compatibility
    91  		{{.Software}} service run --network NETWORK --no-version-check
    92  	`)
    93  )
    94  
    95  type ServicePreCheck func(rf *RootFlags) error
    96  
    97  type RunServiceHandler func(io.Writer, *RootFlags, *RunServiceFlags) error
    98  
    99  func NewCmdRunService(w io.Writer, rf *RootFlags) *cobra.Command {
   100  	return BuildCmdRunService(w, RunService, rf)
   101  }
   102  
   103  func BuildCmdRunService(w io.Writer, handler RunServiceHandler, rf *RootFlags) *cobra.Command {
   104  	f := &RunServiceFlags{}
   105  
   106  	cmd := &cobra.Command{
   107  		Use:     "run",
   108  		Short:   "Start the Vega wallet service",
   109  		Long:    runServiceLong,
   110  		Example: runServiceExample,
   111  		RunE: func(_ *cobra.Command, _ []string) error {
   112  			if err := f.Validate(rf); err != nil {
   113  				return err
   114  			}
   115  
   116  			if err := handler(w, rf, f); err != nil {
   117  				return err
   118  			}
   119  
   120  			return nil
   121  		},
   122  	}
   123  
   124  	cmd.Flags().StringVarP(&f.Network,
   125  		"network", "n",
   126  		"",
   127  		"Network configuration to use",
   128  	)
   129  	cmd.Flags().BoolVar(&f.EnableAutomaticConsent,
   130  		"automatic-consent",
   131  		false,
   132  		"Automatically approve incoming transaction. Only use this flag when you have absolute trust in incoming transactions!",
   133  	)
   134  	cmd.Flags().BoolVar(&f.LoadTokens,
   135  		"load-tokens",
   136  		false,
   137  		"Load the sessions with long-living tokens",
   138  	)
   139  	cmd.PersistentFlags().BoolVar(&f.NoVersionCheck,
   140  		"no-version-check",
   141  		false,
   142  		"Do not check the network version compatibility",
   143  	)
   144  	cmd.Flags().StringVar(&f.TokensPassphraseFile,
   145  		"tokens-passphrase-file",
   146  		"",
   147  		"Path to the file containing the tokens database passphrase",
   148  	)
   149  
   150  	autoCompleteNetwork(cmd, rf.Home)
   151  
   152  	return cmd
   153  }
   154  
   155  type RunServiceFlags struct {
   156  	Network                string
   157  	EnableAutomaticConsent bool
   158  	LoadTokens             bool
   159  	TokensPassphraseFile   string
   160  	NoVersionCheck         bool
   161  	tokensPassphrase       string
   162  }
   163  
   164  func (f *RunServiceFlags) Validate(rf *RootFlags) error {
   165  	if len(f.Network) == 0 {
   166  		return flags.MustBeSpecifiedError("network")
   167  	}
   168  
   169  	if !f.LoadTokens && f.TokensPassphraseFile != "" {
   170  		return flags.OneOfParentsFlagMustBeSpecifiedError("tokens-passphrase-file", "load-tokens")
   171  	}
   172  
   173  	if f.LoadTokens {
   174  		if err := ensureAPITokenStoreIsInit(rf); err != nil {
   175  			return err
   176  		}
   177  		passphrase, err := flags.GetPassphraseWithOptions(flags.PassphraseOptions{Name: "tokens"}, f.TokensPassphraseFile)
   178  		if err != nil {
   179  			return err
   180  		}
   181  		f.tokensPassphrase = passphrase
   182  	}
   183  
   184  	return nil
   185  }
   186  
   187  func RunService(w io.Writer, rf *RootFlags, f *RunServiceFlags) error {
   188  	if err := ensureNotRunningInMsys(); err != nil {
   189  		return err
   190  	}
   191  
   192  	p := printer.NewInteractivePrinter(w)
   193  
   194  	vegaPaths := paths.New(rf.Home)
   195  
   196  	cliLog, cliLogPath, _, err := buildJSONFileLogger(vegaPaths, paths.WalletCLILogsHome, "info")
   197  	if err != nil {
   198  		return err
   199  	}
   200  	defer vgzap.Sync(cliLog)
   201  	cliLog = cliLog.Named("command")
   202  
   203  	if rf.Output == flags.InteractiveOutput && version.IsUnreleased() {
   204  		cliLog.Warn("Current software is an unreleased version", zap.String("version", coreversion.Get()))
   205  		str := p.String()
   206  		str.CrossMark().DangerText("You are running an unreleased version of the Vega wallet (").DangerText(coreversion.Get()).DangerText(").").NextLine()
   207  		str.Pad().DangerText("Use it at your own risk!").NextSection()
   208  		p.Print(str)
   209  	} else {
   210  		cliLog.Warn("Current software is a released version", zap.String("version", coreversion.Get()))
   211  	}
   212  
   213  	p.Print(p.String().CheckMark().Text("CLI logs located at: ").SuccessText(cliLogPath).NextLine())
   214  
   215  	closer := vgclose.NewCloser()
   216  	defer closer.CloseAll()
   217  
   218  	walletStore, err := wallets.InitialiseStoreFromPaths(vegaPaths, true)
   219  	if err != nil {
   220  		cliLog.Error("Could not initialise wallets store", zap.Error(err))
   221  		return fmt.Errorf("could not initialise wallets store: %w", err)
   222  	}
   223  	closer.Add(walletStore.Close)
   224  
   225  	netStore, err := netStoreV1.InitialiseStore(vegaPaths)
   226  	if err != nil {
   227  		cliLog.Error("Could not initialise network store", zap.Error(err))
   228  		return fmt.Errorf("could not initialise network store: %w", err)
   229  	}
   230  
   231  	svcStore, err := svcStoreV1.InitialiseStore(vegaPaths)
   232  	if err != nil {
   233  		cliLog.Error("Could not initialise service store", zap.Error(err))
   234  		return fmt.Errorf("could not initialise service store: %w", err)
   235  	}
   236  
   237  	sessionStore, err := sessionStoreV1.InitialiseStore(vegaPaths)
   238  	if err != nil {
   239  		cliLog.Error("Could not initialise session store", zap.Error(err))
   240  		return fmt.Errorf("could not initialise session store: %w", err)
   241  	}
   242  
   243  	var tokenStore connections.TokenStore
   244  	if f.LoadTokens {
   245  		cliLog.Warn("Long-living tokens enabled")
   246  		p.Print(p.String().WarningBangMark().WarningText("Long-living tokens enabled").NextLine())
   247  		s, err := tokenStoreV1.InitialiseStore(vegaPaths, f.tokensPassphrase)
   248  		if err != nil {
   249  			if errors.Is(err, tokenStoreV1.ErrWrongPassphrase) {
   250  				return err
   251  			}
   252  			return fmt.Errorf("couldn't load the token store: %w", err)
   253  		}
   254  		closer.Add(s.Close)
   255  		tokenStore = s
   256  	} else {
   257  		s := tokenStoreV1.NewEmptyStore()
   258  		tokenStore = s
   259  	}
   260  
   261  	loggerBuilderFunc := func(levelName string) (*zap.Logger, zap.AtomicLevel, error) {
   262  		svcLog, svcLogPath, level, err := buildJSONFileLogger(vegaPaths, paths.WalletServiceLogsHome, levelName)
   263  		if err != nil {
   264  			return nil, zap.AtomicLevel{}, err
   265  		}
   266  
   267  		p.Print(p.String().CheckMark().Text("Service logs located at: ").SuccessText(svcLogPath).NextLine())
   268  
   269  		return svcLog, level, nil
   270  	}
   271  
   272  	consentRequests := make(chan serviceV1.ConsentRequest, MaxConsentRequests)
   273  	sentTransactions := make(chan serviceV1.SentTransaction)
   274  	closer.Add(func() {
   275  		close(consentRequests)
   276  		close(sentTransactions)
   277  	})
   278  
   279  	jobRunner := vgjob.NewRunner(context.Background())
   280  
   281  	policy, err := buildPolicy(jobRunner.Ctx(), cliLog, p, f, consentRequests, sentTransactions)
   282  	if err != nil {
   283  		return err
   284  	}
   285  
   286  	receptionChanForParking := make(chan interactor.Interaction, 1000)
   287  	closer.Add(func() {
   288  		close(receptionChanForParking)
   289  	})
   290  
   291  	seqInteractor := interactor.NewParallelInteractor(jobRunner.Ctx(), receptionChanForParking)
   292  
   293  	connectionsManager, err := connections.NewManager(serviceV2.NewStdTime(), walletStore, tokenStore, sessionStore, seqInteractor)
   294  	if err != nil {
   295  		return fmt.Errorf("could not create the connection manager: %w", err)
   296  	}
   297  	closer.Add(func() {
   298  		connectionsManager.EndAllSessionConnections()
   299  	})
   300  
   301  	serviceStarter := service.NewStarter(walletStore, netStore, svcStore, connectionsManager, policy, seqInteractor, loggerBuilderFunc)
   302  
   303  	rc, err := serviceStarter.Start(jobRunner, f.Network, f.NoVersionCheck)
   304  	if err != nil {
   305  		cliLog.Error("Failed to start HTTP server", zap.Error(err))
   306  		jobRunner.StopAllJobs()
   307  		return err
   308  	}
   309  
   310  	cliLog.Info("Starting HTTP service", zap.String("url", rc.ServiceURL))
   311  	p.Print(p.String().CheckMark().Text("Starting HTTP service at: ").SuccessText(rc.ServiceURL).NextSection())
   312  
   313  	receptionChanForFrontend := make(chan interactor.Interaction, 1000)
   314  	closer.Add(func() {
   315  		close(receptionChanForFrontend)
   316  	})
   317  
   318  	jobRunner.Go(func(jobCtx context.Context) {
   319  		startInteractionParking(cliLog, jobCtx, receptionChanForParking, receptionChanForFrontend)
   320  	})
   321  
   322  	jobRunner.Go(func(jobCtx context.Context) {
   323  		for {
   324  			select {
   325  			case <-jobCtx.Done():
   326  				cliLog.Info("Stop listening to incoming interactions in front-end")
   327  				return
   328  			case interaction := <-receptionChanForFrontend:
   329  				handleAPIv2Request(jobCtx, interaction, f.EnableAutomaticConsent, p)
   330  			case consentRequest := <-consentRequests:
   331  				handleAPIv1Request(consentRequest, cliLog, p, sentTransactions)
   332  			}
   333  		}
   334  	})
   335  
   336  	waitUntilInterruption(jobRunner.Ctx(), cliLog, p, rc.ErrCh)
   337  
   338  	// Wait for all goroutine to exit.
   339  	cliLog.Info("Waiting for the service to stop")
   340  	p.Print(p.String().BlueArrow().Text("Waiting for the service to stop...").NextLine())
   341  	jobRunner.StopAllJobs()
   342  	cliLog.Info("The service stopped")
   343  	p.Print(p.String().CheckMark().Text("The service stopped.").NextLine())
   344  
   345  	return nil
   346  }
   347  
   348  func buildPolicy(ctx context.Context, cliLog *zap.Logger, p *printer.InteractivePrinter, f *RunServiceFlags, consentRequests chan serviceV1.ConsentRequest, sentTransactions chan serviceV1.SentTransaction) (serviceV1.Policy, error) {
   349  	if vgterm.HasTTY() {
   350  		cliLog.Info("TTY detected")
   351  		if f.EnableAutomaticConsent {
   352  			cliLog.Info("Automatic consent enabled")
   353  			p.Print(p.String().WarningBangMark().WarningText("Automatic consent enabled").NextLine())
   354  			return serviceV1.NewAutomaticConsentPolicy(), nil
   355  		}
   356  		cliLog.Info("Explicit consent enabled")
   357  		p.Print(p.String().CheckMark().Text("Explicit consent enabled").NextLine())
   358  		return serviceV1.NewExplicitConsentPolicy(ctx, consentRequests, sentTransactions), nil
   359  	}
   360  
   361  	cliLog.Info("No TTY detected")
   362  
   363  	if !f.EnableAutomaticConsent {
   364  		cliLog.Error("Explicit consent can't be used when no TTY is attached to the process")
   365  		return nil, ErrEnableAutomaticConsentFlagIsRequiredWithoutTTY
   366  	}
   367  
   368  	cliLog.Info("Automatic consent enabled.")
   369  	return serviceV1.NewAutomaticConsentPolicy(), nil
   370  }
   371  
   372  func buildJSONFileLogger(vegaPaths paths.Paths, logDir paths.StatePath, logLevel string) (*zap.Logger, string, zap.AtomicLevel, error) {
   373  	loggerConfig := vgzap.DefaultConfig()
   374  	loggerConfig = vgzap.WithFileOutputForDedicatedProcess(loggerConfig, vegaPaths.StatePathFor(logDir))
   375  	logFilePath := loggerConfig.OutputPaths[0]
   376  	loggerConfig = vgzap.WithJSONFormat(loggerConfig)
   377  	loggerConfig = vgzap.WithLevel(loggerConfig, logLevel)
   378  
   379  	level := loggerConfig.Level
   380  
   381  	logger, err := vgzap.Build(loggerConfig)
   382  	if err != nil {
   383  		return nil, "", zap.AtomicLevel{}, fmt.Errorf("could not setup the logger: %w", err)
   384  	}
   385  
   386  	return logger, logFilePath, level, nil
   387  }
   388  
   389  // waitUntilInterruption will wait for a sigterm or sigint interrupt.
   390  func waitUntilInterruption(ctx context.Context, cliLog *zap.Logger, p *printer.InteractivePrinter, errChan <-chan error) {
   391  	gracefulStop := make(chan os.Signal, 1)
   392  	defer func() {
   393  		signal.Stop(gracefulStop)
   394  		close(gracefulStop)
   395  	}()
   396  
   397  	signal.Notify(gracefulStop, syscall.SIGTERM, syscall.SIGINT, syscall.SIGQUIT)
   398  
   399  	for {
   400  		select {
   401  		case sig := <-gracefulStop:
   402  			cliLog.Info("OS signal received", zap.String("signal", fmt.Sprintf("%+v", sig)))
   403  			str := p.String()
   404  			str.NextSection().WarningBangMark().WarningText(fmt.Sprintf("Signal \"%+v\" received.", sig)).NextLine()
   405  			str.Pad().WarningText("You can hit CTRL+C once again to forcefully exit, but some resources may not be properly cleaned up.").NextSection()
   406  			p.Print(str)
   407  			return
   408  		case err := <-errChan:
   409  			cliLog.Error("Initiating shutdown due to an internal error reported by the service", zap.Error(err))
   410  			return
   411  		case <-ctx.Done():
   412  			cliLog.Info("Stop listening to OS signals")
   413  			return
   414  		}
   415  	}
   416  }
   417  
   418  func handleAPIv1Request(consentRequest serviceV1.ConsentRequest, log *zap.Logger, p *printer.InteractivePrinter, sentTransactions chan serviceV1.SentTransaction) {
   419  	m := jsonpb.Marshaler{Indent: "    "}
   420  	marshalledTx, err := m.MarshalToString(consentRequest.Tx)
   421  	if err != nil {
   422  		log.Error("could not marshal transaction from consent request", zap.Error(err))
   423  		panic(err)
   424  	}
   425  
   426  	str := p.String()
   427  	str.BlueArrow().Text("New transaction received: ").NextLine()
   428  	str.InfoText(marshalledTx).NextLine()
   429  	p.Print(str)
   430  
   431  	if flags.DoYouApproveTx() {
   432  		log.Info("user approved the signing of the transaction", zap.Any("transaction", marshalledTx))
   433  		consentRequest.Confirmation <- serviceV1.ConsentConfirmation{Decision: true}
   434  		p.Print(p.String().CheckMark().SuccessText("Transaction approved").NextLine())
   435  
   436  		sentTx := <-sentTransactions
   437  		log.Info("transaction sent", zap.Any("ID", sentTx.TxID), zap.Any("hash", sentTx.TxHash))
   438  		if sentTx.Error != nil {
   439  			log.Error("transaction failed", zap.Any("transaction", marshalledTx))
   440  			p.Print(p.String().DangerBangMark().DangerText("Transaction failed").NextLine())
   441  			p.Print(p.String().DangerBangMark().DangerText("Error: ").DangerText(sentTx.Error.Error()).NextSection())
   442  		} else {
   443  			log.Info("transaction sent", zap.Any("hash", sentTx.TxHash))
   444  			p.Print(p.String().CheckMark().Text("Transaction with hash ").SuccessText(sentTx.TxHash).Text(" sent!").NextSection())
   445  		}
   446  	} else {
   447  		log.Info("user rejected the signing of the transaction", zap.Any("transaction", marshalledTx))
   448  		consentRequest.Confirmation <- serviceV1.ConsentConfirmation{Decision: false}
   449  		p.Print(p.String().DangerBangMark().DangerText("Transaction rejected").NextSection())
   450  	}
   451  }
   452  
   453  func handleAPIv2Request(ctx context.Context, interaction interactor.Interaction, enableAutomaticConsent bool, p *printer.InteractivePrinter) {
   454  	switch data := interaction.Data.(type) {
   455  	case interactor.InteractionSessionBegan:
   456  		p.Print(p.String().NextLine())
   457  	case interactor.InteractionSessionEnded:
   458  		p.Print(p.String().NextLine())
   459  	case interactor.RequestWalletConnectionReview:
   460  		p.Print(p.String().BlueArrow().Text("The application \"").InfoText(data.Hostname).Text("\" wants to connect to your wallet.").NextLine())
   461  		var connectionApproval string
   462  		approved, err := yesOrNo(ctx, data.ControlCh, p.String().QuestionMark().Text("Do you approve connecting your wallet to this application?"), p)
   463  		if err != nil {
   464  			p.Print(p.String().CrossMark().DangerText(err.Error()).NextLine())
   465  			return
   466  		}
   467  		if approved {
   468  			p.Print(p.String().CheckMark().Text("Connection approved.").NextLine())
   469  			connectionApproval = string(preferences.ApprovedOnlyThisTime)
   470  		} else {
   471  			p.Print(p.String().CrossMark().Text("Connection rejected.").NextLine())
   472  			connectionApproval = string(preferences.RejectedOnlyThisTime)
   473  		}
   474  		data.ResponseCh <- interactor.Interaction{
   475  			TraceID: interaction.TraceID,
   476  			Name:    interactor.WalletConnectionDecisionName,
   477  			Data: interactor.WalletConnectionDecision{
   478  				ConnectionApproval: connectionApproval,
   479  			},
   480  		}
   481  	case interactor.RequestWalletSelection:
   482  		str := p.String().BlueArrow().Text("Here are the available wallets:").NextLine()
   483  		for _, w := range data.AvailableWallets {
   484  			str.ListItem().Text("- ").InfoText(w).NextLine()
   485  		}
   486  		p.Print(str)
   487  		selectedWallet, err := readInput(ctx, data.ControlCh, p.String().QuestionMark().Text("Which wallet do you want to use? "), p, data.AvailableWallets)
   488  		if err != nil {
   489  			p.Print(p.String().CrossMark().DangerText(err.Error()).NextLine())
   490  			return
   491  		}
   492  		data.ResponseCh <- interactor.Interaction{
   493  			TraceID: interaction.TraceID,
   494  			Name:    interactor.SelectedWalletName,
   495  			Data: interactor.SelectedWallet{
   496  				Wallet: selectedWallet,
   497  			},
   498  		}
   499  	case interactor.RequestPassphrase:
   500  		if len(data.Reason) != 0 {
   501  			str := p.String().BlueArrow().Text(data.Reason).NextLine()
   502  			p.Print(str)
   503  		}
   504  		passphrase, err := readPassphrase(ctx, data.ControlCh, p.String().BlueArrow().Text("Enter the passphrase for the wallet \"").InfoText(data.Wallet).Text("\": "), p)
   505  		if err != nil {
   506  			p.Print(p.String().CrossMark().DangerText(err.Error()).NextLine())
   507  			return
   508  		}
   509  		data.ResponseCh <- interactor.Interaction{
   510  			TraceID: interaction.TraceID,
   511  			Name:    interactor.EnteredPassphraseName,
   512  			Data: interactor.EnteredPassphrase{
   513  				Passphrase: passphrase,
   514  			},
   515  		}
   516  	case interactor.ErrorOccurred:
   517  		if data.Type == string(walletapi.InternalErrorType) {
   518  			str := p.String().DangerBangMark().DangerText("An internal error occurred: ").DangerText(data.Error).NextLine()
   519  			str.DangerBangMark().DangerText("The request has been canceled.").NextLine()
   520  			p.Print(str)
   521  		} else if data.Type == string(walletapi.UserErrorType) {
   522  			p.Print(p.String().DangerBangMark().DangerText(data.Error).NextLine())
   523  		} else {
   524  			p.Print(p.String().DangerBangMark().DangerText(fmt.Sprintf("Error: %s (%s)", data.Error, data.Type)).NextLine())
   525  		}
   526  	case interactor.Log:
   527  		str := p.String()
   528  		switch data.Type {
   529  		case string(walletapi.InfoLog):
   530  			str.BlueArrow()
   531  		case string(walletapi.ErrorLog):
   532  			str.CrossMark()
   533  		case string(walletapi.WarningLog):
   534  			str.WarningBangMark()
   535  		case string(walletapi.SuccessLog):
   536  			str.CheckMark()
   537  		default:
   538  			str.Text("- ")
   539  		}
   540  		p.Print(str.Text(data.Message).NextLine())
   541  	case interactor.RequestSucceeded:
   542  		if data.Message == "" {
   543  			p.Print(p.String().CheckMark().SuccessText("Request succeeded").NextLine())
   544  		} else {
   545  			p.Print(p.String().CheckMark().SuccessText(data.Message).NextLine())
   546  		}
   547  	case interactor.RequestPermissionsReview:
   548  		str := p.String().BlueArrow().Text("The application \"").InfoText(data.Hostname).Text("\" requires the following permissions for \"").InfoText(data.Wallet).Text("\":").NextLine()
   549  		for perm, access := range data.Permissions {
   550  			str.ListItem().Text("- ").InfoText(perm).Text(": ").InfoText(access).NextLine()
   551  		}
   552  		p.Print(str)
   553  		approved, err := yesOrNo(ctx, data.ControlCh, p.String().QuestionMark().Text("Do you want to grant these permissions?"), p)
   554  		if err != nil {
   555  			p.Print(p.String().CrossMark().DangerText(err.Error()).NextLine())
   556  			return
   557  		}
   558  		if approved {
   559  			p.Print(p.String().CheckMark().Text("Permissions update approved.").NextLine())
   560  		} else {
   561  			p.Print(p.String().CrossMark().Text("Permissions update rejected.").NextLine())
   562  		}
   563  		data.ResponseCh <- interactor.Interaction{
   564  			TraceID: interaction.TraceID,
   565  			Name:    interactor.DecisionName,
   566  			Data: interactor.Decision{
   567  				Approved: approved,
   568  			},
   569  		}
   570  	case interactor.RequestTransactionReviewForSending:
   571  		str := p.String().BlueArrow().Text("The application \"").InfoText(data.Hostname).Text("\" wants to send the following transaction:").NextLine()
   572  		str.Pad().Text("Using the key: ").InfoText(data.PublicKey).NextLine()
   573  		str.Pad().Text("From the wallet: ").InfoText(data.Wallet).NextLine()
   574  		fmtCmd := strings.Replace("  "+data.Transaction, "\n", "\n  ", -1)
   575  		str.InfoText(fmtCmd).NextLine()
   576  		p.Print(str)
   577  		approved := true
   578  		if enableAutomaticConsent {
   579  			p.Print(p.String().CheckMark().Text("Sending automatically approved.").NextLine())
   580  		} else {
   581  			a, err := yesOrNo(ctx, data.ControlCh, p.String().QuestionMark().Text("Do you want to send this transaction?"), p)
   582  			if err != nil {
   583  				p.Print(p.String().CrossMark().DangerText(err.Error()).NextLine())
   584  				return
   585  			}
   586  			approved = a
   587  			if approved {
   588  				p.Print(p.String().CheckMark().Text("Sending approved.").NextLine())
   589  			} else {
   590  				p.Print(p.String().CrossMark().Text("Sending rejected.").NextLine())
   591  			}
   592  		}
   593  		data.ResponseCh <- interactor.Interaction{
   594  			TraceID: interaction.TraceID,
   595  			Name:    interactor.DecisionName,
   596  			Data: interactor.Decision{
   597  				Approved: approved,
   598  			},
   599  		}
   600  	case interactor.RequestTransactionReviewForSigning:
   601  		str := p.String().BlueArrow().Text("The application \"").InfoText(data.Hostname).Text("\" wants to sign the following transaction:").NextLine()
   602  		str.Pad().Text("Using the key: ").InfoText(data.PublicKey).NextLine()
   603  		str.Pad().Text("From the wallet: ").InfoText(data.Wallet).NextLine()
   604  		fmtCmd := strings.Replace("  "+data.Transaction, "\n", "\n  ", -1)
   605  		str.InfoText(fmtCmd).NextLine()
   606  		p.Print(str)
   607  		approved := true
   608  		if enableAutomaticConsent {
   609  			p.Print(p.String().CheckMark().Text("Signing automatically approved.").NextLine())
   610  		} else {
   611  			a, err := yesOrNo(ctx, data.ControlCh, p.String().QuestionMark().Text("Do you want to sign this transaction?"), p)
   612  			if err != nil {
   613  				p.Print(p.String().CrossMark().DangerText(err.Error()).NextLine())
   614  				return
   615  			}
   616  			approved = a
   617  			if approved {
   618  				p.Print(p.String().CheckMark().Text("Signing approved.").NextLine())
   619  			} else {
   620  				p.Print(p.String().CrossMark().Text("Signing rejected.").NextLine())
   621  			}
   622  		}
   623  		data.ResponseCh <- interactor.Interaction{
   624  			TraceID: interaction.TraceID,
   625  			Name:    interactor.DecisionName,
   626  			Data: interactor.Decision{
   627  				Approved: approved,
   628  			},
   629  		}
   630  	case interactor.RequestTransactionReviewForChecking:
   631  		str := p.String().BlueArrow().Text("The application \"").InfoText(data.Hostname).Text("\" wants to check the following transaction:").NextLine()
   632  		str.Pad().Text("Using the key: ").InfoText(data.PublicKey).NextLine()
   633  		str.Pad().Text("From the wallet: ").InfoText(data.Wallet).NextLine()
   634  		fmtCmd := strings.Replace("  "+data.Transaction, "\n", "\n  ", -1)
   635  		str.InfoText(fmtCmd).NextLine()
   636  		p.Print(str)
   637  		approved := true
   638  		if enableAutomaticConsent {
   639  			p.Print(p.String().CheckMark().Text("Checking automatically approved.").NextLine())
   640  		} else {
   641  			a, err := yesOrNo(ctx, data.ControlCh, p.String().QuestionMark().Text("Do you allow the network to check this transaction?"), p)
   642  			if err != nil {
   643  				p.Print(p.String().CrossMark().DangerText(err.Error()).NextLine())
   644  				return
   645  			}
   646  			approved = a
   647  			if approved {
   648  				p.Print(p.String().CheckMark().Text("Checking approved.").NextLine())
   649  			} else {
   650  				p.Print(p.String().CrossMark().Text("Checking rejected.").NextLine())
   651  			}
   652  		}
   653  		data.ResponseCh <- interactor.Interaction{
   654  			TraceID: interaction.TraceID,
   655  			Name:    interactor.DecisionName,
   656  			Data: interactor.Decision{
   657  				Approved: approved,
   658  			},
   659  		}
   660  	case interactor.TransactionFailed:
   661  		str := p.String()
   662  		str.DangerBangMark().DangerText("The transaction failed.").NextLine()
   663  		str.Pad().DangerText(data.Error.Error()).NextLine()
   664  		str.Pad().Text("Sent at: ").Text(data.SentAt.Format(time.ANSIC)).NextLine()
   665  		p.Print(str)
   666  	case interactor.TransactionSucceeded:
   667  		str := p.String()
   668  		str.CheckMark().SuccessText("The transaction has been delivered.").NextLine()
   669  		str.Pad().Text("Transaction hash: ").SuccessText(data.TxHash).NextLine()
   670  		str.Pad().Text("Sent at: ").Text(data.SentAt.Format(time.ANSIC)).NextLine()
   671  		p.Print(str)
   672  	default:
   673  		panic(fmt.Sprintf("unhandled interaction: %q", interaction.Name))
   674  	}
   675  }
   676  
   677  func readInput(ctx context.Context, controlCh chan error, question *printer.FormattedString, p *printer.InteractivePrinter, options []string) (string, error) {
   678  	inputCh := make(chan string)
   679  	defer close(inputCh)
   680  
   681  	reader, err := cancelreader.NewReader(os.Stdin)
   682  	if err != nil {
   683  		return "", fmt.Errorf("could not initialize the input reader: %w", err)
   684  	}
   685  	defer reader.Cancel()
   686  
   687  	go func() {
   688  		for {
   689  			p.Print(question)
   690  
   691  			answer, err := readString(reader)
   692  			if err != nil {
   693  				return
   694  			}
   695  
   696  			if len(options) == 0 {
   697  				inputCh <- answer
   698  				return
   699  			}
   700  			for _, option := range options {
   701  				if answer == option {
   702  					inputCh <- answer
   703  					return
   704  				}
   705  			}
   706  			if len(answer) > 0 {
   707  				p.Print(p.String().DangerBangMark().DangerText(fmt.Sprintf("%q is not a valid option", answer)).NextLine())
   708  			}
   709  		}
   710  	}()
   711  
   712  	select {
   713  	case <-ctx.Done():
   714  		return "", ctx.Err()
   715  	case err := <-controlCh:
   716  		reader.Cancel()
   717  		return "", err
   718  	case input := <-inputCh:
   719  		return input, nil
   720  	}
   721  }
   722  
   723  func yesOrNo(ctx context.Context, controlCh <-chan error, question *printer.FormattedString, p *printer.InteractivePrinter) (bool, error) {
   724  	choiceCh := make(chan bool)
   725  	defer close(choiceCh)
   726  
   727  	reader, err := cancelreader.NewReader(os.Stdin)
   728  	if err != nil {
   729  		return false, fmt.Errorf("could not initialize the input reader: %w", err)
   730  	}
   731  	defer reader.Cancel()
   732  
   733  	go func() {
   734  		question.Text(" (yes/no) ")
   735  
   736  		for {
   737  			p.Print(question)
   738  
   739  			answer, err := readString(reader)
   740  			if err != nil {
   741  				return
   742  			}
   743  
   744  			answer = strings.ToLower(answer)
   745  
   746  			switch answer {
   747  			case "yes", "y":
   748  				choiceCh <- true
   749  				return
   750  			case "no", "n":
   751  				choiceCh <- false
   752  				return
   753  			default:
   754  				if len(answer) > 0 {
   755  					p.Print(p.String().DangerBangMark().DangerText(fmt.Sprintf("%q is not a valid answer, enter \"yes\" or \"no\"\n", answer)))
   756  				}
   757  			}
   758  		}
   759  	}()
   760  
   761  	select {
   762  	case <-ctx.Done():
   763  		return false, ctx.Err()
   764  	case err := <-controlCh:
   765  		reader.Cancel()
   766  		return false, err
   767  	case choice, ok := <-choiceCh:
   768  		return ok && choice, nil
   769  	}
   770  }
   771  
   772  func readString(reader io.Reader) (string, error) {
   773  	var line string
   774  	for {
   775  		var input [50]byte
   776  		bytesRead, err := reader.Read(input[:])
   777  
   778  		// As said in the Read documentation:
   779  		//     Callers should treat a return of 0 and nil as indicating that
   780  		//     nothing happened; in particular it does not indicate EOF.
   781  		if bytesRead == 0 && err == nil {
   782  			continue
   783  		}
   784  
   785  		if bytesRead == 0 && err != nil && !errors.Is(err, io.EOF) {
   786  			return "", err
   787  		}
   788  
   789  		// As said in the Read documentation:
   790  		//     Callers should always process the n > 0 bytes returned before
   791  		//     considering the error err. Doing so correctly handles I/O errors
   792  		//     that happen after reading some bytes and also both of the
   793  		//     allowed EOF behaviors.
   794  		line += string(input[:bytesRead])
   795  
   796  		// Verify if the input chunk contains the "enter" key.
   797  		if strings.ContainsAny(line, "\r\n") || err != nil {
   798  			break
   799  		}
   800  	}
   801  
   802  	return strings.Trim(line, " \r\n\t"), nil // nolint:nilerr
   803  }
   804  
   805  // ensureNotRunningInMsys verifies if the underlying shell is not running on
   806  // msys.
   807  // This command is not supported on msys, due to some system incompatibilities
   808  // with the user input management.
   809  // Non-exhaustive list of affected systems: Cygwin, minty, git-bash.
   810  func ensureNotRunningInMsys() error {
   811  	ms := os.Getenv("MSYSTEM")
   812  	if ms != "" {
   813  		return ErrMsysUnsupported
   814  	}
   815  	return nil
   816  }
   817  
   818  func readPassphrase(ctx context.Context, controlCh chan error, question *printer.FormattedString, p *printer.InteractivePrinter) (string, error) {
   819  	stdinFd := int(os.Stdin.Fd())
   820  
   821  	inputCh := make(chan string)
   822  	originalState, err := term.GetState(stdinFd)
   823  	if err != nil {
   824  		return "", fmt.Errorf("could not acquire the standard input's original state: %w", err)
   825  	}
   826  	defer func() {
   827  		close(inputCh)
   828  		if err := term.Restore(stdinFd, originalState); err != nil {
   829  			p.Print(p.String().WarningBangMark().WarningText(err.Error()).NextLine())
   830  		}
   831  	}()
   832  
   833  	// We cannot interrupt cleanly an on-going password read. So, at least, we
   834  	// ensure it can stop on the next password attempt.
   835  	shouldStop := atomic.Bool{}
   836  	waitForExitInput := make(chan interface{})
   837  
   838  	go func() {
   839  		for {
   840  			p.Print(question)
   841  			passphrase, err := term.ReadPassword(stdinFd)
   842  			if err != nil {
   843  				panic(fmt.Errorf("could not read passphrase: %w", err))
   844  			}
   845  			p.Print(p.String().NextLine())
   846  			if shouldStop.Load() {
   847  				close(waitForExitInput)
   848  				return
   849  			}
   850  			if len(passphrase) > 0 {
   851  				inputCh <- string(passphrase)
   852  				return
   853  			}
   854  		}
   855  	}()
   856  
   857  	select {
   858  	case <-ctx.Done():
   859  		return "", ctx.Err()
   860  	case err := <-controlCh:
   861  		shouldStop.Store(true)
   862  		<-waitForExitInput
   863  		return "", err
   864  	case input := <-inputCh:
   865  		return input, nil
   866  	}
   867  }
   868  
   869  func startInteractionParking(log *zap.Logger, ctx context.Context, inboundCh <-chan interactor.Interaction, outboundCh chan<- interactor.Interaction) {
   870  	sessionsOrder := []string{}
   871  	parkedInteractionSessions := map[string]chan interactor.Interaction{}
   872  
   873  	defer func() {
   874  		for _, iChan := range parkedInteractionSessions {
   875  			close(iChan)
   876  		}
   877  	}()
   878  
   879  	for {
   880  		select {
   881  		case <-ctx.Done():
   882  			log.Info("Stop listening to incoming interactions in parking")
   883  			return
   884  		case interaction, ok := <-inboundCh:
   885  			if !ok {
   886  				return
   887  			}
   888  
   889  			if len(sessionsOrder) == 0 {
   890  				sessionsOrder = append(sessionsOrder, interaction.TraceID)
   891  			}
   892  
   893  			// If the interaction we receive is from the session currently
   894  			// handled in the frontend, we transmit it immediately.
   895  			if sessionsOrder[0] == interaction.TraceID {
   896  				outboundCh <- interaction
   897  				// If this is the last interaction for the current session, we
   898  				// free up the resources and transmit the next session to the UI.
   899  				if _, ok := interaction.Data.(interactor.InteractionSessionEnded); ok {
   900  					sessionsOrder = switchToNextSession(sessionsOrder, parkedInteractionSessions, outboundCh)
   901  				}
   902  			} else {
   903  				// If not, then we park it until the current session end.
   904  				parkedSessionCh, ok := parkedInteractionSessions[interaction.TraceID]
   905  				if !ok {
   906  					// First time we see this session, we track it.
   907  					parkedSessionCh = make(chan interactor.Interaction, 100)
   908  					parkedInteractionSessions[interaction.TraceID] = parkedSessionCh
   909  					sessionsOrder = append(sessionsOrder, interaction.TraceID)
   910  				}
   911  				parkedSessionCh <- interaction
   912  			}
   913  		}
   914  	}
   915  }
   916  
   917  func switchToNextSession(sessionsOrder []string, parkedInteractionSessions map[string]chan interactor.Interaction, outboundCh chan<- interactor.Interaction) []string {
   918  	// Pop this session out the queue, and move onto the next
   919  	// session.
   920  	sessionsOrder = sessionsOrder[1:]
   921  
   922  	if len(sessionsOrder) == 0 {
   923  		return sessionsOrder
   924  	}
   925  
   926  	currentSessionCh := parkedInteractionSessions[sessionsOrder[0]]
   927  	hasInteractionsToSend := true
   928  	for hasInteractionsToSend {
   929  		select {
   930  		case currentSessionInteraction, ok := <-currentSessionCh:
   931  			if !ok {
   932  				hasInteractionsToSend = false
   933  				break
   934  			}
   935  			outboundCh <- currentSessionInteraction
   936  			if _, ok := currentSessionInteraction.Data.(interactor.InteractionSessionEnded); ok {
   937  				// We remove the session and its interactions buffer from the
   938  				// parked ones, because the next interactions we will receive
   939  				// for that session will be transmitted immediately.
   940  				close(currentSessionCh)
   941  				delete(parkedInteractionSessions, sessionsOrder[0])
   942  
   943  				// The session is already finished, move to the next until we
   944  				// transmitted all parked session or until a session is ongoing.
   945  				return switchToNextSession(sessionsOrder, parkedInteractionSessions, outboundCh)
   946  			}
   947  		default:
   948  			hasInteractionsToSend = false
   949  		}
   950  	}
   951  
   952  	// We remove the session and its interactions buffer from the
   953  	// parked ones, because the next interactions we will receive
   954  	// for that session will be transmitted immediately.
   955  	close(currentSessionCh)
   956  	delete(parkedInteractionSessions, sessionsOrder[0])
   957  	return sessionsOrder
   958  }