code.vegaprotocol.io/vega@v0.79.0/cmd/vegawallet/commands/transaction_check.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  	"encoding/json"
    21  	"errors"
    22  	"fmt"
    23  	"io"
    24  	"time"
    25  
    26  	"code.vegaprotocol.io/vega/cmd/vegawallet/commands/cli"
    27  	"code.vegaprotocol.io/vega/cmd/vegawallet/commands/flags"
    28  	"code.vegaprotocol.io/vega/cmd/vegawallet/commands/printer"
    29  	vgzap "code.vegaprotocol.io/vega/libs/zap"
    30  	"code.vegaprotocol.io/vega/paths"
    31  	coreversion "code.vegaprotocol.io/vega/version"
    32  	"code.vegaprotocol.io/vega/wallet/api"
    33  	walletnode "code.vegaprotocol.io/vega/wallet/api/node"
    34  	networkStore "code.vegaprotocol.io/vega/wallet/network/store/v1"
    35  	"code.vegaprotocol.io/vega/wallet/version"
    36  	"code.vegaprotocol.io/vega/wallet/wallets"
    37  
    38  	"github.com/spf13/cobra"
    39  	"go.uber.org/zap"
    40  	"go.uber.org/zap/zapcore"
    41  )
    42  
    43  var (
    44  	checkTransactionLong = cli.LongDesc(`
    45  		Check a transaction via the gRPC API. The transaction can be sent to
    46  		any node of a registered network or to a specific node address.
    47  
    48  		The transaction should be a Vega transaction formatted as a JSON payload, as follows:
    49  
    50  		'{"commandName": {"someProperty": "someValue"} }'
    51  
    52  		For vote submission, it will look like this:
    53  
    54  		'{"voteSubmission": {"proposalId": "some-id", "value": "VALUE_YES"}}'
    55  	`)
    56  
    57  	checkTransactionExample = cli.Examples(`
    58  		# Check a transaction and send it to a registered network
    59  		{{.Software}} transaction check --network NETWORK --wallet WALLET --pubkey PUBKEY TRANSACTION
    60  
    61  		# Check a transaction and send it to a specific Vega node address
    62  		{{.Software}} transaction check --node-address ADDRESS --wallet WALLET --pubkey PUBKEY TRANSACTION
    63  
    64  		# Check a transaction with a log level set to debug
    65  		{{.Software}} transaction check --network NETWORK --wallet WALLET --pubkey PUBKEY --level debug TRANSACTION
    66  
    67  		# Check a transaction with a maximum of 10 retries
    68  		{{.Software}} transaction check --network NETWORK --wallet WALLET --pubkey PUBKEY --retries 10 TRANSACTION
    69  
    70  		# Check a transaction with a maximum request duration of 10 seconds
    71  		{{.Software}} transaction check --network NETWORK --wallet WALLET --pubkey PUBKEY --max-request-duration "10s" TRANSACTION
    72  
    73  		# Check a transaction and send it to a registered network without verifying network version compatibility
    74  		{{.Software}} transaction check --network NETWORK --wallet WALLET --pubkey PUBKEY --no-version-check TRANSACTION
    75  	`)
    76  )
    77  
    78  type CheckTransactionHandler func(api.AdminCheckTransactionParams, string, *zap.Logger) (api.AdminCheckTransactionResult, error)
    79  
    80  func NewCmdCheckTransaction(w io.Writer, rf *RootFlags) *cobra.Command {
    81  	handler := func(params api.AdminCheckTransactionParams, passphrase string, log *zap.Logger) (api.AdminCheckTransactionResult, error) {
    82  		ctx := context.Background()
    83  
    84  		vegaPaths := paths.New(rf.Home)
    85  
    86  		walletStore, err := wallets.InitialiseStore(rf.Home, false)
    87  		if err != nil {
    88  			return api.AdminCheckTransactionResult{}, fmt.Errorf("couldn't initialise wallets store: %w", err)
    89  		}
    90  		defer walletStore.Close()
    91  
    92  		ns, err := networkStore.InitialiseStore(vegaPaths)
    93  		if err != nil {
    94  			return api.AdminCheckTransactionResult{}, fmt.Errorf("couldn't initialise network store: %w", err)
    95  		}
    96  
    97  		if _, errDetails := api.NewAdminUnlockWallet(walletStore).Handle(ctx, api.AdminUnlockWalletParams{
    98  			Wallet:     params.Wallet,
    99  			Passphrase: passphrase,
   100  		}); errDetails != nil {
   101  			return api.AdminCheckTransactionResult{}, errors.New(errDetails.Data)
   102  		}
   103  
   104  		checkTx := api.NewAdminCheckTransaction(walletStore, ns, func(hosts []string, retries uint64, requestTTL time.Duration) (walletnode.Selector, error) {
   105  			return walletnode.BuildRoundRobinSelectorWithRetryingNodes(log, hosts, retries, requestTTL)
   106  		})
   107  
   108  		rawResult, errDetails := checkTx.Handle(ctx, params)
   109  		if errDetails != nil {
   110  			return api.AdminCheckTransactionResult{}, errors.New(errDetails.Data)
   111  		}
   112  		return rawResult.(api.AdminCheckTransactionResult), nil
   113  	}
   114  
   115  	return BuildCmdCheckTransaction(w, handler, rf)
   116  }
   117  
   118  func BuildCmdCheckTransaction(w io.Writer, handler CheckTransactionHandler, rf *RootFlags) *cobra.Command {
   119  	f := &CheckTransactionFlags{}
   120  
   121  	cmd := &cobra.Command{
   122  		Use:     "check",
   123  		Short:   "Check a transaction and send it to a Vega node",
   124  		Long:    checkTransactionLong,
   125  		Example: checkTransactionExample,
   126  		RunE: func(_ *cobra.Command, args []string) error {
   127  			if aLen := len(args); aLen == 0 {
   128  				return flags.ArgMustBeSpecifiedError("transaction")
   129  			} else if aLen > 1 {
   130  				return flags.TooManyArgsError("transaction")
   131  			}
   132  			f.RawTransaction = args[0]
   133  
   134  			req, pass, err := f.Validate()
   135  			if err != nil {
   136  				return err
   137  			}
   138  
   139  			log, err := buildCmdLogger(rf.Output, f.LogLevel)
   140  			if err != nil {
   141  				return fmt.Errorf("failed to build a logger: %w", err)
   142  			}
   143  
   144  			resp, err := handler(req, pass, log)
   145  			if err != nil {
   146  				return err
   147  			}
   148  
   149  			switch rf.Output {
   150  			case flags.InteractiveOutput:
   151  				PrintCheckTransactionResponse(w, resp, rf)
   152  			case flags.JSONOutput:
   153  				return printer.FprintJSON(w, resp)
   154  			}
   155  			return nil
   156  		},
   157  	}
   158  
   159  	cmd.Flags().StringVarP(&f.Network,
   160  		"network", "n",
   161  		"",
   162  		"Network to send the transaction to after it is checked",
   163  	)
   164  	cmd.Flags().StringVar(&f.NodeAddress,
   165  		"node-address",
   166  		"",
   167  		"Vega node address to which the transaction is sent after it is checked",
   168  	)
   169  	cmd.Flags().StringVarP(&f.Wallet,
   170  		"wallet", "w",
   171  		"",
   172  		"Wallet holding the public key",
   173  	)
   174  	cmd.Flags().StringVarP(&f.PubKey,
   175  		"pubkey", "k",
   176  		"",
   177  		"Public key of the key pair to use for signing (hex-encoded)",
   178  	)
   179  	cmd.Flags().StringVarP(&f.PassphraseFile,
   180  		"passphrase-file", "p",
   181  		"",
   182  		"Path to the file containing the wallet's passphrase",
   183  	)
   184  	cmd.Flags().StringVar(&f.LogLevel,
   185  		"level",
   186  		zapcore.InfoLevel.String(),
   187  		fmt.Sprintf("Set the log level: %v", vgzap.SupportedLogLevels),
   188  	)
   189  	cmd.Flags().Uint64Var(&f.Retries,
   190  		"retries",
   191  		defaultRequestRetryCount,
   192  		"Number of retries when contacting the Vega node",
   193  	)
   194  	cmd.Flags().DurationVar(&f.MaximumRequestDuration,
   195  		"max-request-duration",
   196  		defaultMaxRequestDuration,
   197  		"Maximum duration the wallet will wait for a node to respond. Supported format: <number>+<time unit>. Valid time units are `s` and `m`.",
   198  	)
   199  	cmd.Flags().BoolVar(&f.NoVersionCheck,
   200  		"no-version-check",
   201  		false,
   202  		"Do not check for network version compatibility",
   203  	)
   204  
   205  	autoCompleteNetwork(cmd, rf.Home)
   206  	autoCompleteWallet(cmd, rf.Home, "wallet")
   207  	autoCompleteLogLevel(cmd)
   208  
   209  	return cmd
   210  }
   211  
   212  type CheckTransactionFlags struct {
   213  	Network                string
   214  	NodeAddress            string
   215  	Wallet                 string
   216  	PubKey                 string
   217  	PassphraseFile         string
   218  	Retries                uint64
   219  	LogLevel               string
   220  	RawTransaction         string
   221  	NoVersionCheck         bool
   222  	MaximumRequestDuration time.Duration
   223  }
   224  
   225  func (f *CheckTransactionFlags) Validate() (api.AdminCheckTransactionParams, string, error) {
   226  	if len(f.Wallet) == 0 {
   227  		return api.AdminCheckTransactionParams{}, "", flags.MustBeSpecifiedError("wallet")
   228  	}
   229  
   230  	if len(f.LogLevel) == 0 {
   231  		return api.AdminCheckTransactionParams{}, "", flags.MustBeSpecifiedError("level")
   232  	}
   233  	if err := vgzap.EnsureIsSupportedLogLevel(f.LogLevel); err != nil {
   234  		return api.AdminCheckTransactionParams{}, "", err
   235  	}
   236  
   237  	if len(f.NodeAddress) == 0 && len(f.Network) == 0 {
   238  		return api.AdminCheckTransactionParams{}, "", flags.OneOfFlagsMustBeSpecifiedError("network", "node-address")
   239  	}
   240  
   241  	if len(f.NodeAddress) != 0 && len(f.Network) != 0 {
   242  		return api.AdminCheckTransactionParams{}, "", flags.MutuallyExclusiveError("network", "node-address")
   243  	}
   244  
   245  	if len(f.PubKey) == 0 {
   246  		return api.AdminCheckTransactionParams{}, "", flags.MustBeSpecifiedError("pubkey")
   247  	}
   248  
   249  	if len(f.RawTransaction) == 0 {
   250  		return api.AdminCheckTransactionParams{}, "", flags.ArgMustBeSpecifiedError("transaction")
   251  	}
   252  
   253  	passphrase, err := flags.GetPassphrase(f.PassphraseFile)
   254  	if err != nil {
   255  		return api.AdminCheckTransactionParams{}, "", err
   256  	}
   257  
   258  	// Encode transaction into a nested structure; this is a bit nasty but mirroring what happens
   259  	// when our json-rpc library parses a request. There's an issue (6983#) to make the use
   260  	// json.RawMessage instead.
   261  	transaction := make(map[string]any)
   262  	if err := json.Unmarshal([]byte(f.RawTransaction), &transaction); err != nil {
   263  		return api.AdminCheckTransactionParams{}, "", fmt.Errorf("could not unmarshal transaction: %w", err)
   264  	}
   265  
   266  	params := api.AdminCheckTransactionParams{
   267  		Wallet:                 f.Wallet,
   268  		PublicKey:              f.PubKey,
   269  		Network:                f.Network,
   270  		NodeAddress:            f.NodeAddress,
   271  		Retries:                f.Retries,
   272  		MaximumRequestDuration: f.MaximumRequestDuration,
   273  		Transaction:            transaction,
   274  	}
   275  
   276  	return params, passphrase, nil
   277  }
   278  
   279  func PrintCheckTransactionResponse(w io.Writer, res api.AdminCheckTransactionResult, rf *RootFlags) {
   280  	p := printer.NewInteractivePrinter(w)
   281  
   282  	if rf.Output == flags.InteractiveOutput && version.IsUnreleased() {
   283  		str := p.String()
   284  		str.CrossMark().DangerText("You are running an unreleased version of the Vega wallet (").DangerText(coreversion.Get()).DangerText(").").NextLine()
   285  		str.Pad().DangerText("Use it at your own risk!").NextSection()
   286  		p.Print(str)
   287  	}
   288  
   289  	str := p.String()
   290  	defer p.Print(str)
   291  	str.CheckMark().SuccessText("Transaction checking successful").NextSection()
   292  	str.Text("Sent at:").NextLine().WarningText(res.SentAt.Format(time.ANSIC)).NextSection()
   293  	str.Text("Selected node:").NextLine().WarningText(res.Node.Host).NextLine()
   294  }