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 }