code.vegaprotocol.io/vega@v0.79.0/cmd/vegawallet/commands/raw_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/base64"
    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  	commandspb "code.vegaprotocol.io/vega/protos/vega/commands/v1"
    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  
    36  	"github.com/golang/protobuf/proto"
    37  	"github.com/spf13/cobra"
    38  	"go.uber.org/zap"
    39  	"go.uber.org/zap/zapcore"
    40  )
    41  
    42  var (
    43  	sendTxLong = cli.LongDesc(`
    44  		Send a signed 'raw' transaction to a Vega node via the gRPC API. The command can be sent to
    45  		any node of a registered network or to a specific node address.
    46  
    47  		The transaction should be base64-encoded.
    48  	`)
    49  
    50  	sendTxExample = cli.Examples(`
    51  		# Send a transaction to a registered network
    52  		{{.Software}} raw_transaction send --network NETWORK BASE64_TRANSACTION
    53  
    54  		# Send a transaction to a specific Vega node address
    55  		{{.Software}} raw_transaction send --node-address ADDRESS BASE64_TRANSACTION
    56  
    57  		# Send a transaction with a log level set to debug
    58  		{{.Software}} raw_transaction send --network NETWORK --level debug BASE64_TRANSACTION
    59  
    60  		# Send a transaction with a maximum of 10 retries
    61  		{{.Software}} raw_transaction send --network NETWORK --retries 10 BASE64_TRANSACTION
    62  
    63  		# Send a transaction with a maximum request duration of 10 seconds
    64  		{{.Software}} raw_transaction send --network NETWORK --max-request-duration "10s" BASE64_TRANSACTION
    65  
    66  		# Send a transaction without verifying network version compatibility
    67  		{{.Software}} raw_transaction send --network NETWORK --retries 10 BASE64_TRANSACTION --no-version-check
    68  	`)
    69  )
    70  
    71  type SendRawTransactionHandler func(api.AdminSendRawTransactionParams, *zap.Logger) (api.AdminSendRawTransactionResult, error)
    72  
    73  func NewCmdRawTransactionSend(w io.Writer, rf *RootFlags) *cobra.Command {
    74  	h := func(params api.AdminSendRawTransactionParams, log *zap.Logger) (api.AdminSendRawTransactionResult, error) {
    75  		vegaPaths := paths.New(rf.Home)
    76  
    77  		netStore, err := networkStore.InitialiseStore(vegaPaths)
    78  		if err != nil {
    79  			return api.AdminSendRawTransactionResult{}, fmt.Errorf("could not initialise network store: %w", err)
    80  		}
    81  
    82  		sendTransaction := api.NewAdminSendRawTransaction(netStore, func(hosts []string, retries uint64, requestTTL time.Duration) (walletnode.Selector, error) {
    83  			return walletnode.BuildRoundRobinSelectorWithRetryingNodes(log, hosts, retries, requestTTL)
    84  		})
    85  		rawResult, errorDetails := sendTransaction.Handle(context.Background(), params)
    86  		if errorDetails != nil {
    87  			return api.AdminSendRawTransactionResult{}, errors.New(errorDetails.Data)
    88  		}
    89  		return rawResult.(api.AdminSendRawTransactionResult), nil
    90  	}
    91  	return BuildCmdRawTransactionSend(w, h, rf)
    92  }
    93  
    94  func BuildCmdRawTransactionSend(w io.Writer, handler SendRawTransactionHandler, rf *RootFlags) *cobra.Command {
    95  	f := &SendRawTransactionFlags{}
    96  
    97  	cmd := &cobra.Command{
    98  		Use:     "send",
    99  		Short:   "Send a raw transaction to a Vega node",
   100  		Long:    sendTxLong,
   101  		Example: sendTxExample,
   102  		RunE: func(_ *cobra.Command, args []string) error {
   103  			if aLen := len(args); aLen == 0 {
   104  				return flags.ArgMustBeSpecifiedError("transaction")
   105  			} else if aLen > 1 {
   106  				return flags.TooManyArgsError("transaction")
   107  			}
   108  			f.RawTx = args[0]
   109  
   110  			req, err := f.Validate()
   111  			if err != nil {
   112  				return err
   113  			}
   114  
   115  			log, err := buildCmdLogger(rf.Output, f.LogLevel)
   116  			if err != nil {
   117  				return fmt.Errorf("could not initialise logger: %w", err)
   118  			}
   119  			defer vgzap.Sync(log)
   120  
   121  			resp, err := handler(req, log)
   122  			if err != nil {
   123  				return err
   124  			}
   125  
   126  			switch rf.Output {
   127  			case flags.InteractiveOutput:
   128  				PrintTXSendResponse(w, resp)
   129  			case flags.JSONOutput:
   130  				return printer.FprintJSON(w, resp)
   131  			}
   132  
   133  			return nil
   134  		},
   135  	}
   136  
   137  	cmd.Flags().StringVarP(&f.Network,
   138  		"network", "n",
   139  		"",
   140  		"Network to which the command is sent",
   141  	)
   142  	cmd.Flags().StringVar(&f.NodeAddress,
   143  		"node-address",
   144  		"",
   145  		"Vega node address to which the command is sent",
   146  	)
   147  	cmd.Flags().StringVar(&f.LogLevel,
   148  		"level",
   149  		zapcore.InfoLevel.String(),
   150  		fmt.Sprintf("Set the log level: %v", vgzap.SupportedLogLevels),
   151  	)
   152  	cmd.Flags().Uint64Var(&f.Retries,
   153  		"retries",
   154  		defaultRequestRetryCount,
   155  		"Number of retries when contacting the Vega node",
   156  	)
   157  	cmd.Flags().DurationVar(&f.MaximumRequestDuration,
   158  		"max-request-duration",
   159  		defaultMaxRequestDuration,
   160  		"Maximum duration the wallet will wait for a node to respond. Supported format: <number>+<time unit>. Valid time units are `s` and `m`.",
   161  	)
   162  	cmd.Flags().BoolVar(&f.NoVersionCheck,
   163  		"no-version-check",
   164  		false,
   165  		"Do not check for network version compatibility",
   166  	)
   167  
   168  	autoCompleteNetwork(cmd, rf.Home)
   169  	autoCompleteLogLevel(cmd)
   170  	return cmd
   171  }
   172  
   173  type SendRawTransactionFlags struct {
   174  	Network                string
   175  	NodeAddress            string
   176  	Retries                uint64
   177  	LogLevel               string
   178  	RawTx                  string
   179  	NoVersionCheck         bool
   180  	MaximumRequestDuration time.Duration
   181  }
   182  
   183  func (f *SendRawTransactionFlags) Validate() (api.AdminSendRawTransactionParams, error) {
   184  	req := api.AdminSendRawTransactionParams{
   185  		Retries:                f.Retries,
   186  		MaximumRequestDuration: f.MaximumRequestDuration,
   187  	}
   188  
   189  	if len(f.LogLevel) == 0 {
   190  		return api.AdminSendRawTransactionParams{}, flags.MustBeSpecifiedError("level")
   191  	}
   192  	if err := vgzap.EnsureIsSupportedLogLevel(f.LogLevel); err != nil {
   193  		return api.AdminSendRawTransactionParams{}, err
   194  	}
   195  
   196  	if len(f.NodeAddress) == 0 && len(f.Network) == 0 {
   197  		return api.AdminSendRawTransactionParams{}, flags.OneOfFlagsMustBeSpecifiedError("network", "node-address")
   198  	}
   199  	if len(f.NodeAddress) != 0 && len(f.Network) != 0 {
   200  		return api.AdminSendRawTransactionParams{}, flags.MutuallyExclusiveError("network", "node-address")
   201  	}
   202  	req.NodeAddress = f.NodeAddress
   203  	req.Network = f.Network
   204  	req.SendingMode = "TYPE_ASYNC"
   205  
   206  	if len(f.RawTx) == 0 {
   207  		return api.AdminSendRawTransactionParams{}, flags.ArgMustBeSpecifiedError("transaction")
   208  	}
   209  	decodedTx, err := base64.StdEncoding.DecodeString(f.RawTx)
   210  	if err != nil {
   211  		return api.AdminSendRawTransactionParams{}, flags.MustBase64EncodedError("transaction")
   212  	}
   213  	tx := &commandspb.Transaction{}
   214  	if err := proto.Unmarshal(decodedTx, tx); err != nil {
   215  		return api.AdminSendRawTransactionParams{}, fmt.Errorf("could not unmarshal transaction: %w", err)
   216  	}
   217  	req.EncodedTransaction = f.RawTx
   218  
   219  	return req, nil
   220  }
   221  
   222  func PrintTXSendResponse(w io.Writer, res api.AdminSendRawTransactionResult) {
   223  	p := printer.NewInteractivePrinter(w)
   224  
   225  	str := p.String()
   226  	defer p.Print(str)
   227  	str.CheckMark().SuccessText("Transaction successfully sent").NextSection()
   228  	str.Text("Transaction Hash:").NextLine().WarningText(res.TransactionHash).NextSection()
   229  	str.Text("Sent at:").NextLine().WarningText(res.SentAt.Format(time.ANSIC)).NextSection()
   230  	str.Text("Selected node:").NextLine().WarningText(res.Node.Host).NextLine()
   231  }