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