code.vegaprotocol.io/vega@v0.79.0/cmd/vegawallet/commands/transaction_sign.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 cmd 17 18 import ( 19 "context" 20 "encoding/json" 21 "errors" 22 "fmt" 23 "io" 24 "os" 25 "time" 26 27 "code.vegaprotocol.io/vega/cmd/vegawallet/commands/cli" 28 "code.vegaprotocol.io/vega/cmd/vegawallet/commands/flags" 29 "code.vegaprotocol.io/vega/cmd/vegawallet/commands/printer" 30 "code.vegaprotocol.io/vega/paths" 31 coreversion "code.vegaprotocol.io/vega/version" 32 "code.vegaprotocol.io/vega/wallet/api" 33 walletnode "code.vegaprotocol.io/vega/wallet/api/node" 34 networkStore "code.vegaprotocol.io/vega/wallet/network/store/v1" 35 "code.vegaprotocol.io/vega/wallet/version" 36 "code.vegaprotocol.io/vega/wallet/wallets" 37 38 "github.com/spf13/cobra" 39 "go.uber.org/zap" 40 ) 41 42 var ( 43 signTransactionLong = cli.LongDesc(` 44 Sign a transaction using the specified wallet and public key and bundle it as a 45 raw transaction ready to be sent. The resulting transaction is base64-encoded and 46 can be sent using the command "raw_transaction send". 47 48 The transaction should be a Vega transaction formatted as a JSON payload, as follows: 49 50 '{"commandName": {"someProperty": "someValue"} }' 51 52 For vote submission, it will look like this: 53 54 '{"voteSubmission": {"proposalId": "some-id", "value": "VALUE_YES"}}' 55 56 Providing a network will allow the signed transaction to contain a valid 57 proof-of-work generated and attached automatically. If using in an offline 58 environment then proof-of-work details should be supplied via the CLI options. 59 `) 60 61 signTransactionExample = cli.Examples(` 62 # Sign a transaction offline with necessary information to generate a proof-of-work 63 {{.Software}} transaction sign --wallet WALLET --pubkey PUBKEY --tx-height TX_HEIGHT --chain-id CHAIN_ID --tx-block-hash BLOCK_HASH --pow-difficulty POW_DIFF --pow-difficulty "sha3_24_rounds" TRANSACTION 64 65 # Sign a transaction online generating proof-of-work automatically using the network to obtain the last block data 66 {{.Software}} transaction sign --wallet WALLET --pubkey PUBKEY --network NETWORK TRANSACTION 67 68 # To decode the result, save the result in a file and use the command 69 # "base64" 70 {{.Software}} transaction sign --wallet WALLET --pubkey PUBKEY --network NETWORK TRANSACTION > result.txt 71 base64 --decode --input result.txt 72 73 # Sign a transaction online with a maximum request duration of 10 seconds 74 {{.Software}} transaction sign --wallet WALLET --pubkey PUBKEY --network NETWORK --max-request-duration "10s" TRANSACTION 75 `) 76 ) 77 78 type SignTransactionHandler func(api.AdminSignTransactionParams, string, *zap.Logger) (api.AdminSignTransactionResult, error) 79 80 func NewCmdSignTransaction(w io.Writer, rf *RootFlags) *cobra.Command { 81 handler := func(params api.AdminSignTransactionParams, passphrase string, log *zap.Logger) (api.AdminSignTransactionResult, error) { 82 ctx := context.Background() 83 84 vegaPaths := paths.New(rf.Home) 85 86 walletStore, err := wallets.InitialiseStore(rf.Home, false) 87 if err != nil { 88 return api.AdminSignTransactionResult{}, fmt.Errorf("couldn't initialise wallets store: %w", err) 89 } 90 defer walletStore.Close() 91 92 ns, err := networkStore.InitialiseStore(vegaPaths) 93 if err != nil { 94 return api.AdminSignTransactionResult{}, fmt.Errorf("couldn't initialise network store: %w", err) 95 } 96 97 if _, errDetails := api.NewAdminUnlockWallet(walletStore).Handle(ctx, api.AdminUnlockWalletParams{ 98 Wallet: params.Wallet, 99 Passphrase: passphrase, 100 }); errDetails != nil { 101 return api.AdminSignTransactionResult{}, errors.New(errDetails.Data) 102 } 103 104 signTx := api.NewAdminSignTransaction(walletStore, ns, func(hosts []string, retries uint64, requestTTL time.Duration) (walletnode.Selector, error) { 105 return walletnode.BuildRoundRobinSelectorWithRetryingNodes(log, hosts, retries, requestTTL) 106 }) 107 108 rawResult, errDetails := signTx.Handle(ctx, params) 109 if errDetails != nil { 110 return api.AdminSignTransactionResult{}, errors.New(errDetails.Data) 111 } 112 return rawResult.(api.AdminSignTransactionResult), nil 113 } 114 115 return BuildCmdSignTransaction(w, handler, rf) 116 } 117 118 func BuildCmdSignTransaction(w io.Writer, handler SignTransactionHandler, rf *RootFlags) *cobra.Command { 119 f := &SignTransactionFlags{} 120 121 cmd := &cobra.Command{ 122 Use: "sign", 123 Short: "Sign a transaction for offline use", 124 Long: signTransactionLong, 125 Example: signTransactionExample, 126 RunE: func(_ *cobra.Command, args []string) error { 127 if aLen := len(args); aLen == 0 { 128 return flags.ArgMustBeSpecifiedError("transaction") 129 } else if aLen > 1 { 130 return flags.TooManyArgsError("transaction") 131 } 132 f.RawTransaction = args[0] 133 134 req, pass, err := f.Validate() 135 if err != nil { 136 return err 137 } 138 139 log, err := buildCmdLogger(rf.Output, "info") 140 if err != nil { 141 return fmt.Errorf("failed to build a logger: %w", err) 142 } 143 144 resp, err := handler(req, pass, log) 145 if err != nil { 146 return err 147 } 148 149 switch rf.Output { 150 case flags.InteractiveOutput: 151 PrintSignTransactionResponse(w, resp, rf) 152 case flags.JSONOutput: 153 return printer.FprintJSON(w, resp) 154 } 155 156 return nil 157 }, 158 } 159 160 cmd.Flags().StringVarP(&f.Wallet, 161 "wallet", "w", 162 "", 163 "Wallet holding the public key", 164 ) 165 cmd.Flags().StringVarP(&f.PubKey, 166 "pubkey", "k", 167 "", 168 "Public key of the key pair to use for signing (hex-encoded)", 169 ) 170 cmd.Flags().StringVarP(&f.PassphraseFile, 171 "passphrase-file", "p", 172 "", 173 "Path to the file containing the wallet's passphrase", 174 ) 175 cmd.Flags().Uint64Var(&f.TxBlockHeight, 176 "tx-height", 177 0, 178 "It should be close to the current block height when the transaction is applied, with a threshold of ~ - 150 blocks, not required if --network is set", 179 ) 180 cmd.Flags().StringVar(&f.ChainID, 181 "chain-id", 182 "", 183 "The identifier of the chain on which the command will be sent to, not required if --network is set", 184 ) 185 cmd.Flags().StringVar(&f.TxBlockHash, 186 "tx-block-hash", 187 "", 188 "The block-hash corresponding to tx-height which will be used to generate proof-of-work (hex encoded)", 189 ) 190 cmd.Flags().Uint32Var(&f.PowDifficulty, 191 "pow-difficulty", 192 0, 193 "The proof-of-work difficulty level", 194 ) 195 cmd.Flags().StringVar(&f.PowHashFunction, 196 "pow-hash-function", 197 "", 198 "The proof-of-work hash function to use to compute the proof-of-work", 199 ) 200 cmd.Flags().StringVar(&f.Network, 201 "network", 202 "", 203 "The network the transaction will be sent to", 204 ) 205 cmd.Flags().Uint64Var(&f.Retries, 206 "retries", 207 defaultRequestRetryCount, 208 "Number of retries when contacting the Vega node", 209 ) 210 cmd.Flags().DurationVar(&f.MaximumRequestDuration, 211 "max-request-duration", 212 defaultMaxRequestDuration, 213 "Maximum duration the wallet will wait for a node to respond. Supported format: <number>+<time unit>. Valid time units are `s` and `m`.", 214 ) 215 216 autoCompleteWallet(cmd, rf.Home, "wallet") 217 218 return cmd 219 } 220 221 type SignTransactionFlags struct { 222 Wallet string 223 PubKey string 224 PassphraseFile string 225 RawTransaction string 226 TxBlockHeight uint64 227 ChainID string 228 TxBlockHash string 229 PowDifficulty uint32 230 PowHashFunction string 231 Network string 232 Retries uint64 233 MaximumRequestDuration time.Duration 234 } 235 236 func (f *SignTransactionFlags) Validate() (api.AdminSignTransactionParams, string, error) { 237 params := api.AdminSignTransactionParams{ 238 MaximumRequestDuration: f.MaximumRequestDuration, 239 Retries: f.Retries, 240 } 241 242 if len(f.Wallet) == 0 { 243 return api.AdminSignTransactionParams{}, "", flags.MustBeSpecifiedError("wallet") 244 } 245 params.Wallet = f.Wallet 246 247 if len(f.PubKey) == 0 { 248 return api.AdminSignTransactionParams{}, "", flags.MustBeSpecifiedError("pubkey") 249 } 250 if len(f.RawTransaction) == 0 { 251 return api.AdminSignTransactionParams{}, "", flags.ArgMustBeSpecifiedError("transaction") 252 } 253 254 if f.Network == "" { 255 if f.TxBlockHeight == 0 { 256 return api.AdminSignTransactionParams{}, "", flags.MustBeSpecifiedError("tx-height") 257 } 258 259 if f.TxBlockHash == "" { 260 return api.AdminSignTransactionParams{}, "", flags.MustBeSpecifiedError("tx-block-hash") 261 } 262 263 if f.ChainID == "" { 264 return api.AdminSignTransactionParams{}, "", flags.MustBeSpecifiedError("chain-id") 265 } 266 if f.PowDifficulty == 0 { 267 return api.AdminSignTransactionParams{}, "", flags.MustBeSpecifiedError("pow-difficulty") 268 } 269 if f.PowHashFunction == "" { 270 return api.AdminSignTransactionParams{}, "", flags.MustBeSpecifiedError("pow-hash-function") 271 } 272 // populate proof-of-work bits 273 params.LastBlockData = &api.AdminLastBlockData{ 274 ChainID: f.ChainID, 275 BlockHeight: f.TxBlockHeight, 276 BlockHash: f.TxBlockHash, 277 ProofOfWorkDifficulty: f.PowDifficulty, 278 ProofOfWorkHashFunction: f.PowHashFunction, 279 } 280 } 281 282 if f.Network != "" { 283 if f.TxBlockHeight != 0 { 284 return api.AdminSignTransactionParams{}, "", flags.MutuallyExclusiveError("network", "tx-height") 285 } 286 if f.TxBlockHash != "" { 287 return api.AdminSignTransactionParams{}, "", flags.MutuallyExclusiveError("network", "tx-block-hash") 288 } 289 if f.ChainID != "" { 290 return api.AdminSignTransactionParams{}, "", flags.MutuallyExclusiveError("network", "chain-id") 291 } 292 if f.PowDifficulty != 0 { 293 return api.AdminSignTransactionParams{}, "", flags.MutuallyExclusiveError("network", "pow-difficulty") 294 } 295 if f.PowHashFunction != "" { 296 return api.AdminSignTransactionParams{}, "", flags.MutuallyExclusiveError("network", "pow-hash-function") 297 } 298 } 299 300 passphrase, err := flags.GetPassphrase(f.PassphraseFile) 301 if err != nil { 302 return api.AdminSignTransactionParams{}, "", err 303 } 304 305 params.Network = f.Network 306 params.PublicKey = f.PubKey 307 308 // Encode transaction into nested structure; this is a bit nasty but mirroring what happens 309 // when our json-rpc library parses a request. There's an issue (6983#) to make the use 310 // json.RawMessage instead. 311 transaction := make(map[string]any) 312 if err := json.Unmarshal([]byte(f.RawTransaction), &transaction); err != nil { 313 return api.AdminSignTransactionParams{}, "", err 314 } 315 316 params.Transaction = transaction 317 return params, passphrase, nil 318 } 319 320 func PrintSignTransactionResponse(w io.Writer, req api.AdminSignTransactionResult, rf *RootFlags) { 321 p := printer.NewInteractivePrinter(w) 322 323 if rf.Output == flags.InteractiveOutput && version.IsUnreleased() { 324 str := p.String() 325 str.CrossMark().DangerText("You are running an unreleased version of the Vega wallet (").DangerText(coreversion.Get()).DangerText(").").NextLine() 326 str.Pad().DangerText("Use it at your own risk!").NextSection() 327 p.Print(str) 328 } 329 330 str := p.String() 331 defer p.Print(str) 332 str.CheckMark().SuccessText("Transaction signature successful").NextSection() 333 str.Text("Transaction (base64-encoded):").NextLine().WarningText(req.EncodedTransaction).NextSection() 334 335 str.BlueArrow().InfoText("Send a transaction").NextLine() 336 str.Text("To send a raw transaction, see the following transaction:").NextSection() 337 str.Code(fmt.Sprintf("%s raw_transaction send --help", os.Args[0])).NextLine() 338 }