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