code.vegaprotocol.io/vega@v0.79.0/wallet/api/client_send_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  	apipb "code.vegaprotocol.io/vega/protos/vega/api/v1"
    29  	commandspb "code.vegaprotocol.io/vega/protos/vega/commands/v1"
    30  	walletpb "code.vegaprotocol.io/vega/protos/vega/wallet/v1"
    31  	"code.vegaprotocol.io/vega/wallet/api/node"
    32  	wcommands "code.vegaprotocol.io/vega/wallet/commands"
    33  
    34  	"github.com/golang/protobuf/jsonpb"
    35  	"github.com/golang/protobuf/proto"
    36  	"github.com/mitchellh/mapstructure"
    37  )
    38  
    39  type ClientSendTransactionParams struct {
    40  	PublicKey   string      `json:"publicKey"`
    41  	SendingMode string      `json:"sendingMode"`
    42  	Transaction interface{} `json:"transaction"`
    43  }
    44  
    45  type ClientParsedSendTransactionParams struct {
    46  	PublicKey      string
    47  	SendingMode    apipb.SubmitTransactionRequest_Type
    48  	RawTransaction string
    49  }
    50  
    51  type ClientSendTransactionResult struct {
    52  	ReceivedAt      time.Time               `json:"receivedAt"`
    53  	SentAt          time.Time               `json:"sentAt"`
    54  	TransactionHash string                  `json:"transactionHash"`
    55  	Transaction     *commandspb.Transaction `json:"transaction"`
    56  }
    57  
    58  type ClientSendTransaction struct {
    59  	walletStore       WalletStore
    60  	interactor        Interactor
    61  	nodeSelector      node.Selector
    62  	spam              SpamHandler
    63  	requestController *RequestController
    64  }
    65  
    66  func (h *ClientSendTransaction) Handle(ctx context.Context, rawParams jsonrpc.Params, connectedWallet ConnectedWallet) (jsonrpc.Result, *jsonrpc.ErrorDetails) {
    67  	traceID := jsonrpc.TraceIDFromContext(ctx)
    68  
    69  	receivedAt := time.Now()
    70  
    71  	params, err := validateSendTransactionParams(rawParams)
    72  	if err != nil {
    73  		return nil, InvalidParams(err)
    74  	}
    75  
    76  	txReader := strings.NewReader(params.RawTransaction)
    77  	request := &walletpb.SubmitTransactionRequest{}
    78  	if err := jsonpb.Unmarshal(txReader, request); err != nil {
    79  		return nil, InvalidParams(fmt.Errorf("the transaction does not use a valid Vega command: %w", err))
    80  	}
    81  
    82  	if !connectedWallet.CanUseKey(params.PublicKey) {
    83  		return nil, RequestNotPermittedError(ErrPublicKeyIsNotAllowedToBeUsed)
    84  	}
    85  
    86  	w, err := h.walletStore.GetWallet(ctx, connectedWallet.Name())
    87  	if err != nil {
    88  		if errors.Is(err, ErrWalletIsLocked) {
    89  			h.interactor.NotifyError(ctx, traceID, ApplicationErrorType, err)
    90  		} else {
    91  			h.interactor.NotifyError(ctx, traceID, InternalErrorType, fmt.Errorf("could not retrieve the wallet associated to the connection: %w", err))
    92  		}
    93  		return nil, InternalError(ErrCouldNotSignTransaction)
    94  	}
    95  
    96  	request.PubKey = params.PublicKey
    97  	if errs := wcommands.CheckSubmitTransactionRequest(request); !errs.Empty() {
    98  		return nil, InvalidParams(errs)
    99  	}
   100  
   101  	iAmDone, err := h.requestController.IsPublicKeyAlreadyInUse(params.PublicKey)
   102  	if err != nil {
   103  		return nil, RequestNotPermittedError(err)
   104  	}
   105  	defer iAmDone()
   106  
   107  	if err := h.interactor.NotifyInteractionSessionBegan(ctx, traceID, TransactionReviewWorkflow, 2); err != nil {
   108  		return nil, RequestNotPermittedError(err)
   109  	}
   110  	defer h.interactor.NotifyInteractionSessionEnded(ctx, traceID)
   111  
   112  	if connectedWallet.RequireInteraction() {
   113  		approved, err := h.interactor.RequestTransactionReviewForSending(ctx, traceID, 1, connectedWallet.Hostname(), connectedWallet.Name(), params.PublicKey, params.RawTransaction, receivedAt)
   114  		if err != nil {
   115  			if errDetails := HandleRequestFlowError(ctx, traceID, h.interactor, err); errDetails != nil {
   116  				return nil, errDetails
   117  			}
   118  			h.interactor.NotifyError(ctx, traceID, InternalErrorType, fmt.Errorf("requesting the transaction review failed: %w", err))
   119  			return nil, InternalError(ErrCouldNotSendTransaction)
   120  		}
   121  		if !approved {
   122  			return nil, UserRejectionError(ErrUserRejectedSendingOfTransaction)
   123  		}
   124  	} else {
   125  		h.interactor.Log(ctx, traceID, InfoLog, fmt.Sprintf("Trying to send the transaction: %v", request.String()))
   126  	}
   127  
   128  	h.interactor.Log(ctx, traceID, InfoLog, "Looking for a healthy node...")
   129  	currentNode, err := h.nodeSelector.Node(ctx, func(reportType node.ReportType, msg string) {
   130  		h.interactor.Log(ctx, traceID, LogType(reportType), msg)
   131  	})
   132  	if err != nil {
   133  		h.interactor.NotifyError(ctx, traceID, NetworkErrorType, fmt.Errorf("could not find a healthy node: %w", err))
   134  		return nil, NodeCommunicationError(ErrNoHealthyNodeAvailable)
   135  	}
   136  
   137  	h.interactor.Log(ctx, traceID, InfoLog, "Retrieving latest block information...")
   138  	stats, err := currentNode.SpamStatistics(ctx, request.PubKey)
   139  	if err != nil {
   140  		h.interactor.NotifyError(ctx, traceID, NetworkErrorType, fmt.Errorf("could not get the latest spam statistics for the public key from the node: %w", err))
   141  		return nil, NodeCommunicationError(ErrCouldNotGetSpamStatistics)
   142  	}
   143  	h.interactor.Log(ctx, traceID, SuccessLog, "Latest spam statistics  for the public key have been retrieved.")
   144  
   145  	if stats.LastBlockHeight == 0 {
   146  		h.interactor.NotifyError(ctx, traceID, NetworkErrorType, ErrCouldNotGetSpamStatistics)
   147  		return nil, NodeCommunicationError(ErrCouldNotGetSpamStatistics)
   148  	}
   149  
   150  	if stats.ChainID == "" {
   151  		h.interactor.NotifyError(ctx, traceID, NetworkErrorType, ErrCouldNotGetChainIDFromNode)
   152  		return nil, NodeCommunicationError(ErrCouldNotGetChainIDFromNode)
   153  	}
   154  
   155  	h.interactor.Log(ctx, traceID, InfoLog, "Verifying if the transaction passes the anti-spam rules...")
   156  	err = h.spam.CheckSubmission(request, &stats)
   157  	if err != nil {
   158  		h.interactor.NotifyError(ctx, traceID, ApplicationErrorType, fmt.Errorf("could not send transaction: %w", err))
   159  		return nil, ApplicationCancellationError(err)
   160  	}
   161  	h.interactor.Log(ctx, traceID, SuccessLog, "The transaction passes the anti-spam rules.")
   162  
   163  	// Sign the payload.
   164  	rawInputData := wcommands.ToInputData(request, stats.LastBlockHeight)
   165  	inputData, err := commands.MarshalInputData(rawInputData)
   166  	if err != nil {
   167  		h.interactor.NotifyError(ctx, traceID, InternalErrorType, fmt.Errorf("could not marshal input data: %w", err))
   168  		return nil, InternalError(ErrCouldNotSendTransaction)
   169  	}
   170  
   171  	h.interactor.Log(ctx, traceID, InfoLog, "Signing the transaction...")
   172  	signature, err := w.SignTx(params.PublicKey, commands.BundleInputDataForSigning(inputData, stats.ChainID))
   173  	if err != nil {
   174  		h.interactor.NotifyError(ctx, traceID, InternalErrorType, fmt.Errorf("could not sign the transaction: %w", err))
   175  		return nil, InternalError(ErrCouldNotSendTransaction)
   176  	}
   177  	h.interactor.Log(ctx, traceID, SuccessLog, "The transaction has been signed.")
   178  
   179  	// Build the transaction.
   180  	tx := commands.NewTransaction(params.PublicKey, inputData, &commandspb.Signature{
   181  		Value:   signature.Value,
   182  		Algo:    signature.Algo,
   183  		Version: signature.Version,
   184  	})
   185  
   186  	// Generate the proof of work for the transaction.
   187  	h.interactor.Log(ctx, traceID, InfoLog, "Computing proof-of-work...")
   188  	tx.Pow, err = h.spam.GenerateProofOfWork(params.PublicKey, &stats)
   189  	if err != nil {
   190  		if errors.Is(err, ErrTransactionsPerBlockLimitReached) || errors.Is(err, ErrBlockHeightTooHistoric) {
   191  			h.interactor.NotifyError(ctx, traceID, ApplicationErrorType, fmt.Errorf("could not compute the proof-of-work: %w", err))
   192  			return nil, ApplicationCancellationError(err)
   193  		}
   194  		h.interactor.NotifyError(ctx, traceID, InternalErrorType, fmt.Errorf("could not compute the proof-of-work: %w", err))
   195  		return nil, InternalError(ErrCouldNotSendTransaction)
   196  	}
   197  
   198  	h.interactor.Log(ctx, traceID, SuccessLog, "The proof-of-work has been computed.")
   199  	sentAt := time.Now()
   200  
   201  	h.interactor.Log(ctx, traceID, InfoLog, "Sending the transaction to the network...")
   202  	txHash, err := currentNode.SendTransaction(ctx, tx, params.SendingMode)
   203  	if err != nil {
   204  		h.interactor.NotifyFailedTransaction(ctx, traceID, 2, protoToJSON(rawInputData), protoToJSON(tx), err, sentAt, currentNode.Host())
   205  		return nil, NetworkErrorFromTransactionError(err)
   206  	}
   207  
   208  	h.interactor.NotifySuccessfulTransaction(ctx, traceID, 2, txHash, protoToJSON(rawInputData), protoToJSON(tx), sentAt, currentNode.Host())
   209  
   210  	return ClientSendTransactionResult{
   211  		ReceivedAt:      receivedAt,
   212  		SentAt:          sentAt,
   213  		TransactionHash: txHash,
   214  		Transaction:     tx,
   215  	}, nil
   216  }
   217  
   218  func protoToJSON(tx proto.Message) string {
   219  	m := jsonpb.Marshaler{
   220  		EmitDefaults: true,
   221  		Indent:       "  ",
   222  	}
   223  	jsonProto, mErr := m.MarshalToString(tx)
   224  	if mErr != nil {
   225  		// We ignore this error as it's not critical. At least, we can transmit
   226  		// the transaction hash so the client front-end can redirect to the
   227  		// block explorer.
   228  		jsonProto = ""
   229  	}
   230  	return jsonProto
   231  }
   232  
   233  func validateSendTransactionParams(rawParams jsonrpc.Params) (ClientParsedSendTransactionParams, error) {
   234  	if rawParams == nil {
   235  		return ClientParsedSendTransactionParams{}, ErrParamsRequired
   236  	}
   237  
   238  	params := ClientSendTransactionParams{}
   239  	if err := mapstructure.Decode(rawParams, &params); err != nil {
   240  		return ClientParsedSendTransactionParams{}, ErrParamsDoNotMatch
   241  	}
   242  
   243  	if params.PublicKey == "" {
   244  		return ClientParsedSendTransactionParams{}, ErrPublicKeyIsRequired
   245  	}
   246  
   247  	if params.SendingMode == "" {
   248  		return ClientParsedSendTransactionParams{}, ErrSendingModeIsRequired
   249  	}
   250  
   251  	isValidSendingMode := false
   252  	var sendingMode apipb.SubmitTransactionRequest_Type
   253  	for tp, sm := range apipb.SubmitTransactionRequest_Type_value {
   254  		if tp == params.SendingMode {
   255  			isValidSendingMode = true
   256  			sendingMode = apipb.SubmitTransactionRequest_Type(sm)
   257  		}
   258  	}
   259  	if !isValidSendingMode {
   260  		return ClientParsedSendTransactionParams{}, fmt.Errorf("the sending mode %q is not a valid one", params.SendingMode)
   261  	}
   262  
   263  	if sendingMode == apipb.SubmitTransactionRequest_TYPE_UNSPECIFIED {
   264  		return ClientParsedSendTransactionParams{}, ErrSendingModeCannotBeTypeUnspecified
   265  	}
   266  
   267  	if params.Transaction == nil {
   268  		return ClientParsedSendTransactionParams{}, ErrTransactionIsRequired
   269  	}
   270  
   271  	tx, err := json.Marshal(params.Transaction)
   272  	if err != nil {
   273  		return ClientParsedSendTransactionParams{}, ErrTransactionIsNotValidJSON
   274  	}
   275  
   276  	return ClientParsedSendTransactionParams{
   277  		PublicKey:      params.PublicKey,
   278  		RawTransaction: string(tx),
   279  		SendingMode:    sendingMode,
   280  	}, nil
   281  }
   282  
   283  func NewClientSendTransaction(walletStore WalletStore, interactor Interactor, nodeSelector node.Selector, pow SpamHandler, requestController *RequestController) *ClientSendTransaction {
   284  	return &ClientSendTransaction{
   285  		walletStore:       walletStore,
   286  		interactor:        interactor,
   287  		nodeSelector:      nodeSelector,
   288  		spam:              pow,
   289  		requestController: requestController,
   290  	}
   291  }