code.vegaprotocol.io/vega@v0.79.0/wallet/api/client_sign_transaction.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 api
    17  
    18  import (
    19  	"context"
    20  	"encoding/json"
    21  	"errors"
    22  	"fmt"
    23  	"strings"
    24  	"time"
    25  
    26  	"code.vegaprotocol.io/vega/commands"
    27  	"code.vegaprotocol.io/vega/libs/jsonrpc"
    28  	commandspb "code.vegaprotocol.io/vega/protos/vega/commands/v1"
    29  	walletpb "code.vegaprotocol.io/vega/protos/vega/wallet/v1"
    30  	"code.vegaprotocol.io/vega/wallet/api/node"
    31  	wcommands "code.vegaprotocol.io/vega/wallet/commands"
    32  
    33  	"github.com/golang/protobuf/jsonpb"
    34  	"github.com/mitchellh/mapstructure"
    35  )
    36  
    37  const TransactionSuccessfullySigned = "The transaction has been successfully signed."
    38  
    39  type ClientSignTransactionParams struct {
    40  	PublicKey   string      `json:"publicKey"`
    41  	Transaction interface{} `json:"transaction"`
    42  }
    43  
    44  type ClientParsedSignTransactionParams struct {
    45  	PublicKey      string
    46  	RawTransaction string
    47  }
    48  
    49  type ClientSignTransactionResult struct {
    50  	Transaction *commandspb.Transaction `json:"transaction"`
    51  }
    52  
    53  type ClientSignTransaction struct {
    54  	walletStore       WalletStore
    55  	interactor        Interactor
    56  	nodeSelector      node.Selector
    57  	spam              SpamHandler
    58  	requestController *RequestController
    59  }
    60  
    61  func (h *ClientSignTransaction) Handle(ctx context.Context, rawParams jsonrpc.Params, connectedWallet ConnectedWallet) (jsonrpc.Result, *jsonrpc.ErrorDetails) {
    62  	traceID := jsonrpc.TraceIDFromContext(ctx)
    63  
    64  	receivedAt := time.Now()
    65  
    66  	params, err := validateSignTransactionParams(rawParams)
    67  	if err != nil {
    68  		return nil, InvalidParams(err)
    69  	}
    70  
    71  	request := &walletpb.SubmitTransactionRequest{}
    72  	if err := jsonpb.Unmarshal(strings.NewReader(params.RawTransaction), request); err != nil {
    73  		return nil, InvalidParams(fmt.Errorf("the transaction does not use a valid Vega command: %w", err))
    74  	}
    75  
    76  	if !connectedWallet.CanUseKey(params.PublicKey) {
    77  		return nil, RequestNotPermittedError(ErrPublicKeyIsNotAllowedToBeUsed)
    78  	}
    79  
    80  	w, err := h.walletStore.GetWallet(ctx, connectedWallet.Name())
    81  	if err != nil {
    82  		if errors.Is(err, ErrWalletIsLocked) {
    83  			h.interactor.NotifyError(ctx, traceID, ApplicationErrorType, err)
    84  		} else {
    85  			h.interactor.NotifyError(ctx, traceID, InternalErrorType, fmt.Errorf("could not retrieve the wallet associated to the connection: %w", err))
    86  		}
    87  		return nil, InternalError(ErrCouldNotSignTransaction)
    88  	}
    89  
    90  	request.PubKey = params.PublicKey
    91  	if errs := wcommands.CheckSubmitTransactionRequest(request); !errs.Empty() {
    92  		return nil, InvalidParams(errs)
    93  	}
    94  
    95  	iAmDone, err := h.requestController.IsPublicKeyAlreadyInUse(params.PublicKey)
    96  	if err != nil {
    97  		return nil, RequestNotPermittedError(err)
    98  	}
    99  	defer iAmDone()
   100  
   101  	if err := h.interactor.NotifyInteractionSessionBegan(ctx, traceID, TransactionReviewWorkflow, 2); err != nil {
   102  		return nil, RequestNotPermittedError(err)
   103  	}
   104  	defer h.interactor.NotifyInteractionSessionEnded(ctx, traceID)
   105  
   106  	if connectedWallet.RequireInteraction() {
   107  		approved, err := h.interactor.RequestTransactionReviewForSigning(ctx, traceID, 1, connectedWallet.Hostname(), connectedWallet.Name(), params.PublicKey, params.RawTransaction, receivedAt)
   108  		if err != nil {
   109  			if errDetails := HandleRequestFlowError(ctx, traceID, h.interactor, err); errDetails != nil {
   110  				return nil, errDetails
   111  			}
   112  			h.interactor.NotifyError(ctx, traceID, InternalErrorType, fmt.Errorf("requesting the transaction review failed: %w", err))
   113  			return nil, InternalError(ErrCouldNotSignTransaction)
   114  		}
   115  		if !approved {
   116  			return nil, UserRejectionError(ErrUserRejectedSigningOfTransaction)
   117  		}
   118  	} else {
   119  		h.interactor.Log(ctx, traceID, InfoLog, fmt.Sprintf("Trying to sign the transaction: %v", request.String()))
   120  	}
   121  
   122  	h.interactor.Log(ctx, traceID, InfoLog, "Looking for a healthy node...")
   123  	currentNode, err := h.nodeSelector.Node(ctx, func(reportType node.ReportType, msg string) {
   124  		h.interactor.Log(ctx, traceID, LogType(reportType), msg)
   125  	})
   126  	if err != nil {
   127  		h.interactor.NotifyError(ctx, traceID, NetworkErrorType, fmt.Errorf("could not find a healthy node: %w", err))
   128  		return nil, NodeCommunicationError(ErrNoHealthyNodeAvailable)
   129  	}
   130  
   131  	h.interactor.Log(ctx, traceID, InfoLog, "Retrieving latest block information...")
   132  	stats, err := currentNode.SpamStatistics(ctx, request.PubKey)
   133  	if err != nil {
   134  		h.interactor.NotifyError(ctx, traceID, NetworkErrorType, fmt.Errorf("could not get the latest spam statistics for the public key from the node: %w", err))
   135  		return nil, NodeCommunicationError(ErrCouldNotGetSpamStatistics)
   136  	}
   137  	h.interactor.Log(ctx, traceID, SuccessLog, "Latest spam statistics  for the public key have been retrieved.")
   138  
   139  	if stats.LastBlockHeight == 0 {
   140  		h.interactor.NotifyError(ctx, traceID, NetworkErrorType, ErrCouldNotGetSpamStatistics)
   141  		return nil, NodeCommunicationError(ErrCouldNotGetSpamStatistics)
   142  	}
   143  
   144  	if stats.ChainID == "" {
   145  		h.interactor.NotifyError(ctx, traceID, NetworkErrorType, ErrCouldNotGetChainIDFromNode)
   146  		return nil, NodeCommunicationError(ErrCouldNotGetChainIDFromNode)
   147  	}
   148  
   149  	h.interactor.Log(ctx, traceID, InfoLog, "Verifying if the transaction passes the anti-spam rules...")
   150  	err = h.spam.CheckSubmission(request, &stats)
   151  	if err != nil {
   152  		h.interactor.NotifyError(ctx, traceID, ApplicationErrorType, fmt.Errorf("could not send transaction: %w", err))
   153  		return nil, ApplicationCancellationError(err)
   154  	}
   155  	h.interactor.Log(ctx, traceID, SuccessLog, "The transaction passes the anti-spam rules.")
   156  
   157  	// Sign the payload.
   158  	inputData, err := wcommands.ToMarshaledInputData(request, stats.LastBlockHeight)
   159  	if err != nil {
   160  		h.interactor.NotifyError(ctx, traceID, InternalErrorType, fmt.Errorf("could not marshal input data: %w", err))
   161  		return nil, InternalError(ErrCouldNotSignTransaction)
   162  	}
   163  
   164  	h.interactor.Log(ctx, traceID, InfoLog, "Signing the transaction...")
   165  	signature, err := w.SignTx(params.PublicKey, commands.BundleInputDataForSigning(inputData, stats.ChainID))
   166  	if err != nil {
   167  		h.interactor.NotifyError(ctx, traceID, InternalErrorType, fmt.Errorf("could not sign the transaction: %w", err))
   168  		return nil, InternalError(ErrCouldNotSignTransaction)
   169  	}
   170  	h.interactor.Log(ctx, traceID, SuccessLog, "The transaction has been signed.")
   171  
   172  	// Build the transaction.
   173  	tx := commands.NewTransaction(params.PublicKey, inputData, &commandspb.Signature{
   174  		Value:   signature.Value,
   175  		Algo:    signature.Algo,
   176  		Version: signature.Version,
   177  	})
   178  
   179  	// Generate the proof of work for the transaction.
   180  	h.interactor.Log(ctx, traceID, InfoLog, "Computing proof-of-work...")
   181  	tx.Pow, err = h.spam.GenerateProofOfWork(params.PublicKey, &stats)
   182  	if err != nil {
   183  		if errors.Is(err, ErrTransactionsPerBlockLimitReached) || errors.Is(err, ErrBlockHeightTooHistoric) {
   184  			h.interactor.NotifyError(ctx, traceID, ApplicationErrorType, fmt.Errorf("could not compute the proof-of-work: %w", err))
   185  			return nil, ApplicationCancellationError(err)
   186  		}
   187  		h.interactor.NotifyError(ctx, traceID, InternalErrorType, fmt.Errorf("could not compute the proof-of-work: %w", err))
   188  		return nil, InternalError(ErrCouldNotSignTransaction)
   189  	}
   190  	h.interactor.Log(ctx, traceID, SuccessLog, "The proof-of-work has been computed.")
   191  
   192  	h.interactor.NotifySuccessfulRequest(ctx, traceID, 2, TransactionSuccessfullySigned)
   193  
   194  	return ClientSignTransactionResult{
   195  		Transaction: tx,
   196  	}, nil
   197  }
   198  
   199  func validateSignTransactionParams(rawParams jsonrpc.Params) (ClientParsedSignTransactionParams, error) {
   200  	if rawParams == nil {
   201  		return ClientParsedSignTransactionParams{}, ErrParamsRequired
   202  	}
   203  
   204  	params := ClientSignTransactionParams{}
   205  	if err := mapstructure.Decode(rawParams, &params); err != nil {
   206  		return ClientParsedSignTransactionParams{}, ErrParamsDoNotMatch
   207  	}
   208  
   209  	if params.PublicKey == "" {
   210  		return ClientParsedSignTransactionParams{}, ErrPublicKeyIsRequired
   211  	}
   212  
   213  	if params.Transaction == nil {
   214  		return ClientParsedSignTransactionParams{}, ErrTransactionIsRequired
   215  	}
   216  
   217  	tx, err := json.Marshal(params.Transaction)
   218  	if err != nil {
   219  		return ClientParsedSignTransactionParams{}, ErrTransactionIsNotValidJSON
   220  	}
   221  
   222  	return ClientParsedSignTransactionParams{
   223  		PublicKey:      params.PublicKey,
   224  		RawTransaction: string(tx),
   225  	}, nil
   226  }
   227  
   228  func NewClientSignTransaction(walletStore WalletStore, interactor Interactor, nodeSelector node.Selector, proofOfWork SpamHandler, requestController *RequestController) *ClientSignTransaction {
   229  	return &ClientSignTransaction{
   230  		walletStore:       walletStore,
   231  		interactor:        interactor,
   232  		nodeSelector:      nodeSelector,
   233  		spam:              proofOfWork,
   234  		requestController: requestController,
   235  	}
   236  }