code.vegaprotocol.io/vega@v0.79.0/cmd/vegawallet/commands/transaction_send.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  	sendTransactionLong = cli.LongDesc(`
    45  		Send a transaction to a Vega node 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  	sendTransactionExample = cli.Examples(`
    58  		# Send a transaction to a registered network
    59  		{{.Software}} transaction send --network NETWORK --wallet WALLET --pubkey PUBKEY TRANSACTION
    60  
    61  		# Send a transaction to a specific Vega node address
    62  		{{.Software}} transaction send --node-address ADDRESS --wallet WALLET --pubkey PUBKEY TRANSACTION
    63  
    64  		# Send a transaction with a log level set to debug
    65  		{{.Software}} transaction send --network NETWORK --wallet WALLET --pubkey PUBKEY --level debug TRANSACTION
    66  
    67  		# Send a transaction with a maximum of 10 retries
    68  		{{.Software}} transaction send --network NETWORK --wallet WALLET --pubkey PUBKEY --retries 10 TRANSACTION
    69  
    70  		# Send a transaction with a maximum request duration of 10 seconds
    71  		{{.Software}} transaction send --network NETWORK --wallet WALLET --pubkey PUBKEY --max-request-duration "10s" TRANSACTION
    72  
    73  		# Send a transaction to a registered network without verifying network version compatibility
    74  		{{.Software}} transaction send --network NETWORK --wallet WALLET --pubkey PUBKEY --no-version-check TRANSACTION
    75  	`)
    76  )
    77  
    78  type SendTransactionHandler func(api.AdminSendTransactionParams, string, *zap.Logger) (api.AdminSendTransactionResult, error)
    79  
    80  func NewCmdSendTransaction(w io.Writer, rf *RootFlags) *cobra.Command {
    81  	handler := func(params api.AdminSendTransactionParams, passphrase string, log *zap.Logger) (api.AdminSendTransactionResult, 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.AdminSendTransactionResult{}, 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.AdminSendTransactionResult{}, 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.AdminSendTransactionResult{}, errors.New(errDetails.Data)
   102  		}
   103  
   104  		sendTx := api.NewAdminSendTransaction(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 := sendTx.Handle(ctx, params)
   109  		if errDetails != nil {
   110  			return api.AdminSendTransactionResult{}, errors.New(errDetails.Data)
   111  		}
   112  		return rawResult.(api.AdminSendTransactionResult), nil
   113  	}
   114  
   115  	return BuildCmdSendTransaction(w, handler, rf)
   116  }
   117  
   118  func BuildCmdSendTransaction(w io.Writer, handler SendTransactionHandler, rf *RootFlags) *cobra.Command {
   119  	f := &SendTransactionFlags{}
   120  
   121  	cmd := &cobra.Command{
   122  		Use:     "send",
   123  		Short:   "Send a transaction to a Vega node",
   124  		Long:    sendTransactionLong,
   125  		Example: sendTransactionExample,
   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  				PrintSendTransactionResponse(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 which the transaction is sent",
   163  	)
   164  	cmd.Flags().StringVar(&f.NodeAddress,
   165  		"node-address",
   166  		"",
   167  		"Vega node address to which the transaction is sent",
   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 SendTransactionFlags 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 *SendTransactionFlags) Validate() (api.AdminSendTransactionParams, string, error) {
   226  	if len(f.Wallet) == 0 {
   227  		return api.AdminSendTransactionParams{}, "", flags.MustBeSpecifiedError("wallet")
   228  	}
   229  
   230  	if len(f.LogLevel) == 0 {
   231  		return api.AdminSendTransactionParams{}, "", flags.MustBeSpecifiedError("level")
   232  	}
   233  	if err := vgzap.EnsureIsSupportedLogLevel(f.LogLevel); err != nil {
   234  		return api.AdminSendTransactionParams{}, "", err
   235  	}
   236  
   237  	if len(f.NodeAddress) == 0 && len(f.Network) == 0 {
   238  		return api.AdminSendTransactionParams{}, "", flags.OneOfFlagsMustBeSpecifiedError("network", "node-address")
   239  	}
   240  
   241  	if len(f.NodeAddress) != 0 && len(f.Network) != 0 {
   242  		return api.AdminSendTransactionParams{}, "", flags.MutuallyExclusiveError("network", "node-address")
   243  	}
   244  
   245  	if len(f.PubKey) == 0 {
   246  		return api.AdminSendTransactionParams{}, "", flags.MustBeSpecifiedError("pubkey")
   247  	}
   248  
   249  	if len(f.RawTransaction) == 0 {
   250  		return api.AdminSendTransactionParams{}, "", flags.ArgMustBeSpecifiedError("transaction")
   251  	}
   252  
   253  	passphrase, err := flags.GetPassphrase(f.PassphraseFile)
   254  	if err != nil {
   255  		return api.AdminSendTransactionParams{}, "", 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.AdminSendTransactionParams{}, "", fmt.Errorf("couldn't unmarshal transaction: %w", err)
   264  	}
   265  
   266  	params := api.AdminSendTransactionParams{
   267  		Wallet:                 f.Wallet,
   268  		PublicKey:              f.PubKey,
   269  		Network:                f.Network,
   270  		NodeAddress:            f.NodeAddress,
   271  		Retries:                f.Retries,
   272  		Transaction:            transaction,
   273  		SendingMode:            "TYPE_ASYNC",
   274  		MaximumRequestDuration: f.MaximumRequestDuration,
   275  	}
   276  
   277  	return params, passphrase, nil
   278  }
   279  
   280  func PrintSendTransactionResponse(w io.Writer, res api.AdminSendTransactionResult, rf *RootFlags) {
   281  	p := printer.NewInteractivePrinter(w)
   282  
   283  	if rf.Output == flags.InteractiveOutput && version.IsUnreleased() {
   284  		str := p.String()
   285  		str.CrossMark().DangerText("You are running an unreleased version of the Vega wallet (").DangerText(coreversion.Get()).DangerText(").").NextLine()
   286  		str.Pad().DangerText("Use it at your own risk!").NextSection()
   287  		p.Print(str)
   288  	}
   289  
   290  	str := p.String()
   291  	defer p.Print(str)
   292  	str.CheckMark().SuccessText("Transaction sending successful").NextSection()
   293  	str.Text("Transaction Hash:").NextLine().WarningText(res.TransactionHash).NextSection()
   294  	str.Text("Sent at:").NextLine().WarningText(res.SentAt.Format(time.ANSIC)).NextSection()
   295  	str.Text("Selected node:").NextLine().WarningText(res.Node.Host).NextLine()
   296  }