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