code.vegaprotocol.io/vega@v0.79.0/cmd/vegawallet/commands/transaction_sign.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  	"os"
    25  	"time"
    26  
    27  	"code.vegaprotocol.io/vega/cmd/vegawallet/commands/cli"
    28  	"code.vegaprotocol.io/vega/cmd/vegawallet/commands/flags"
    29  	"code.vegaprotocol.io/vega/cmd/vegawallet/commands/printer"
    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  )
    41  
    42  var (
    43  	signTransactionLong = cli.LongDesc(`
    44  		Sign a transaction using the specified wallet and public key and bundle it as a
    45  		raw transaction ready to be sent. The resulting transaction is base64-encoded and
    46  		can be sent using the command "raw_transaction send".
    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  		Providing a network will allow the signed transaction to contain a valid 
    57  		proof-of-work generated and attached automatically. If using in an offline
    58  		environment then proof-of-work details should be supplied via the CLI options.
    59  	`)
    60  
    61  	signTransactionExample = cli.Examples(`
    62  		# Sign a transaction offline with necessary information to generate a proof-of-work
    63  		{{.Software}} transaction sign --wallet WALLET --pubkey PUBKEY --tx-height TX_HEIGHT --chain-id CHAIN_ID --tx-block-hash BLOCK_HASH --pow-difficulty POW_DIFF --pow-difficulty "sha3_24_rounds" TRANSACTION
    64  
    65  		# Sign a transaction online generating proof-of-work automatically using the network to obtain the last block data
    66  		{{.Software}} transaction sign --wallet WALLET --pubkey PUBKEY --network NETWORK TRANSACTION
    67  
    68  		# To decode the result, save the result in a file and use the command
    69  		# "base64"
    70  		{{.Software}} transaction sign --wallet WALLET --pubkey PUBKEY --network NETWORK TRANSACTION > result.txt
    71  		base64 --decode --input result.txt
    72  
    73  		# Sign a transaction online with a maximum request duration of 10 seconds
    74  		{{.Software}} transaction sign --wallet WALLET --pubkey PUBKEY --network NETWORK --max-request-duration "10s" TRANSACTION
    75  	`)
    76  )
    77  
    78  type SignTransactionHandler func(api.AdminSignTransactionParams, string, *zap.Logger) (api.AdminSignTransactionResult, error)
    79  
    80  func NewCmdSignTransaction(w io.Writer, rf *RootFlags) *cobra.Command {
    81  	handler := func(params api.AdminSignTransactionParams, passphrase string, log *zap.Logger) (api.AdminSignTransactionResult, 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.AdminSignTransactionResult{}, 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.AdminSignTransactionResult{}, 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.AdminSignTransactionResult{}, errors.New(errDetails.Data)
   102  		}
   103  
   104  		signTx := api.NewAdminSignTransaction(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 := signTx.Handle(ctx, params)
   109  		if errDetails != nil {
   110  			return api.AdminSignTransactionResult{}, errors.New(errDetails.Data)
   111  		}
   112  		return rawResult.(api.AdminSignTransactionResult), nil
   113  	}
   114  
   115  	return BuildCmdSignTransaction(w, handler, rf)
   116  }
   117  
   118  func BuildCmdSignTransaction(w io.Writer, handler SignTransactionHandler, rf *RootFlags) *cobra.Command {
   119  	f := &SignTransactionFlags{}
   120  
   121  	cmd := &cobra.Command{
   122  		Use:     "sign",
   123  		Short:   "Sign a transaction for offline use",
   124  		Long:    signTransactionLong,
   125  		Example: signTransactionExample,
   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, "info")
   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  				PrintSignTransactionResponse(w, resp, rf)
   152  			case flags.JSONOutput:
   153  				return printer.FprintJSON(w, resp)
   154  			}
   155  
   156  			return nil
   157  		},
   158  	}
   159  
   160  	cmd.Flags().StringVarP(&f.Wallet,
   161  		"wallet", "w",
   162  		"",
   163  		"Wallet holding the public key",
   164  	)
   165  	cmd.Flags().StringVarP(&f.PubKey,
   166  		"pubkey", "k",
   167  		"",
   168  		"Public key of the key pair to use for signing (hex-encoded)",
   169  	)
   170  	cmd.Flags().StringVarP(&f.PassphraseFile,
   171  		"passphrase-file", "p",
   172  		"",
   173  		"Path to the file containing the wallet's passphrase",
   174  	)
   175  	cmd.Flags().Uint64Var(&f.TxBlockHeight,
   176  		"tx-height",
   177  		0,
   178  		"It should be close to the current block height when the transaction is applied, with a threshold of ~ - 150 blocks, not required if --network is set",
   179  	)
   180  	cmd.Flags().StringVar(&f.ChainID,
   181  		"chain-id",
   182  		"",
   183  		"The identifier of the chain on which the command will be sent to, not required if --network is set",
   184  	)
   185  	cmd.Flags().StringVar(&f.TxBlockHash,
   186  		"tx-block-hash",
   187  		"",
   188  		"The block-hash corresponding to tx-height which will be used to generate proof-of-work (hex encoded)",
   189  	)
   190  	cmd.Flags().Uint32Var(&f.PowDifficulty,
   191  		"pow-difficulty",
   192  		0,
   193  		"The proof-of-work difficulty level",
   194  	)
   195  	cmd.Flags().StringVar(&f.PowHashFunction,
   196  		"pow-hash-function",
   197  		"",
   198  		"The proof-of-work hash function to use to compute the proof-of-work",
   199  	)
   200  	cmd.Flags().StringVar(&f.Network,
   201  		"network",
   202  		"",
   203  		"The network the transaction will be sent to",
   204  	)
   205  	cmd.Flags().Uint64Var(&f.Retries,
   206  		"retries",
   207  		defaultRequestRetryCount,
   208  		"Number of retries when contacting the Vega node",
   209  	)
   210  	cmd.Flags().DurationVar(&f.MaximumRequestDuration,
   211  		"max-request-duration",
   212  		defaultMaxRequestDuration,
   213  		"Maximum duration the wallet will wait for a node to respond. Supported format: <number>+<time unit>. Valid time units are `s` and `m`.",
   214  	)
   215  
   216  	autoCompleteWallet(cmd, rf.Home, "wallet")
   217  
   218  	return cmd
   219  }
   220  
   221  type SignTransactionFlags struct {
   222  	Wallet                 string
   223  	PubKey                 string
   224  	PassphraseFile         string
   225  	RawTransaction         string
   226  	TxBlockHeight          uint64
   227  	ChainID                string
   228  	TxBlockHash            string
   229  	PowDifficulty          uint32
   230  	PowHashFunction        string
   231  	Network                string
   232  	Retries                uint64
   233  	MaximumRequestDuration time.Duration
   234  }
   235  
   236  func (f *SignTransactionFlags) Validate() (api.AdminSignTransactionParams, string, error) {
   237  	params := api.AdminSignTransactionParams{
   238  		MaximumRequestDuration: f.MaximumRequestDuration,
   239  		Retries:                f.Retries,
   240  	}
   241  
   242  	if len(f.Wallet) == 0 {
   243  		return api.AdminSignTransactionParams{}, "", flags.MustBeSpecifiedError("wallet")
   244  	}
   245  	params.Wallet = f.Wallet
   246  
   247  	if len(f.PubKey) == 0 {
   248  		return api.AdminSignTransactionParams{}, "", flags.MustBeSpecifiedError("pubkey")
   249  	}
   250  	if len(f.RawTransaction) == 0 {
   251  		return api.AdminSignTransactionParams{}, "", flags.ArgMustBeSpecifiedError("transaction")
   252  	}
   253  
   254  	if f.Network == "" {
   255  		if f.TxBlockHeight == 0 {
   256  			return api.AdminSignTransactionParams{}, "", flags.MustBeSpecifiedError("tx-height")
   257  		}
   258  
   259  		if f.TxBlockHash == "" {
   260  			return api.AdminSignTransactionParams{}, "", flags.MustBeSpecifiedError("tx-block-hash")
   261  		}
   262  
   263  		if f.ChainID == "" {
   264  			return api.AdminSignTransactionParams{}, "", flags.MustBeSpecifiedError("chain-id")
   265  		}
   266  		if f.PowDifficulty == 0 {
   267  			return api.AdminSignTransactionParams{}, "", flags.MustBeSpecifiedError("pow-difficulty")
   268  		}
   269  		if f.PowHashFunction == "" {
   270  			return api.AdminSignTransactionParams{}, "", flags.MustBeSpecifiedError("pow-hash-function")
   271  		}
   272  		// populate proof-of-work bits
   273  		params.LastBlockData = &api.AdminLastBlockData{
   274  			ChainID:                 f.ChainID,
   275  			BlockHeight:             f.TxBlockHeight,
   276  			BlockHash:               f.TxBlockHash,
   277  			ProofOfWorkDifficulty:   f.PowDifficulty,
   278  			ProofOfWorkHashFunction: f.PowHashFunction,
   279  		}
   280  	}
   281  
   282  	if f.Network != "" {
   283  		if f.TxBlockHeight != 0 {
   284  			return api.AdminSignTransactionParams{}, "", flags.MutuallyExclusiveError("network", "tx-height")
   285  		}
   286  		if f.TxBlockHash != "" {
   287  			return api.AdminSignTransactionParams{}, "", flags.MutuallyExclusiveError("network", "tx-block-hash")
   288  		}
   289  		if f.ChainID != "" {
   290  			return api.AdminSignTransactionParams{}, "", flags.MutuallyExclusiveError("network", "chain-id")
   291  		}
   292  		if f.PowDifficulty != 0 {
   293  			return api.AdminSignTransactionParams{}, "", flags.MutuallyExclusiveError("network", "pow-difficulty")
   294  		}
   295  		if f.PowHashFunction != "" {
   296  			return api.AdminSignTransactionParams{}, "", flags.MutuallyExclusiveError("network", "pow-hash-function")
   297  		}
   298  	}
   299  
   300  	passphrase, err := flags.GetPassphrase(f.PassphraseFile)
   301  	if err != nil {
   302  		return api.AdminSignTransactionParams{}, "", err
   303  	}
   304  
   305  	params.Network = f.Network
   306  	params.PublicKey = f.PubKey
   307  
   308  	// Encode transaction into nested structure; this is a bit nasty but mirroring what happens
   309  	// when our json-rpc library parses a request. There's an issue (6983#) to make the use
   310  	// json.RawMessage instead.
   311  	transaction := make(map[string]any)
   312  	if err := json.Unmarshal([]byte(f.RawTransaction), &transaction); err != nil {
   313  		return api.AdminSignTransactionParams{}, "", err
   314  	}
   315  
   316  	params.Transaction = transaction
   317  	return params, passphrase, nil
   318  }
   319  
   320  func PrintSignTransactionResponse(w io.Writer, req api.AdminSignTransactionResult, rf *RootFlags) {
   321  	p := printer.NewInteractivePrinter(w)
   322  
   323  	if rf.Output == flags.InteractiveOutput && version.IsUnreleased() {
   324  		str := p.String()
   325  		str.CrossMark().DangerText("You are running an unreleased version of the Vega wallet (").DangerText(coreversion.Get()).DangerText(").").NextLine()
   326  		str.Pad().DangerText("Use it at your own risk!").NextSection()
   327  		p.Print(str)
   328  	}
   329  
   330  	str := p.String()
   331  	defer p.Print(str)
   332  	str.CheckMark().SuccessText("Transaction signature successful").NextSection()
   333  	str.Text("Transaction (base64-encoded):").NextLine().WarningText(req.EncodedTransaction).NextSection()
   334  
   335  	str.BlueArrow().InfoText("Send a transaction").NextLine()
   336  	str.Text("To send a raw transaction, see the following transaction:").NextSection()
   337  	str.Code(fmt.Sprintf("%s raw_transaction send --help", os.Args[0])).NextLine()
   338  }