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, ¶ms); 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 }