github.com/iotexproject/iotex-core@v1.14.1-rc1/ioctl/newcmd/action/action.go (about) 1 // Copyright (c) 2022 IoTeX Foundation 2 // This source code is provided 'as is' and no warranties are given as to title or non-infringement, merchantability 3 // or fitness for purpose and, to the extent permitted by law, all liability for your use of the code is disclaimed. 4 // This source code is governed by Apache License 2.0 that can be found in the LICENSE file. 5 6 package action 7 8 import ( 9 "context" 10 "encoding/hex" 11 "math/big" 12 "strings" 13 14 "github.com/grpc-ecosystem/go-grpc-middleware/util/metautils" 15 "github.com/iotexproject/go-pkgs/hash" 16 "github.com/iotexproject/iotex-address/address" 17 "github.com/iotexproject/iotex-proto/golang/iotexapi" 18 "github.com/iotexproject/iotex-proto/golang/iotextypes" 19 "github.com/pkg/errors" 20 "github.com/spf13/cobra" 21 "google.golang.org/grpc/codes" 22 "google.golang.org/grpc/status" 23 "google.golang.org/protobuf/proto" 24 25 "github.com/iotexproject/iotex-core/action" 26 "github.com/iotexproject/iotex-core/ioctl" 27 "github.com/iotexproject/iotex-core/ioctl/config" 28 "github.com/iotexproject/iotex-core/ioctl/flag" 29 "github.com/iotexproject/iotex-core/ioctl/newcmd/account" 30 "github.com/iotexproject/iotex-core/ioctl/newcmd/bc" 31 "github.com/iotexproject/iotex-core/ioctl/util" 32 "github.com/iotexproject/iotex-core/pkg/util/byteutil" 33 ) 34 35 // Multi-language support 36 var ( 37 _actionCmdShorts = map[config.Language]string{ 38 config.English: "Manage actions of IoTeX blockchain", 39 config.Chinese: "管理IoTex区块链的行为", // this translation 40 } 41 _infoWarn = map[config.Language]string{ 42 config.English: "** This is an irreversible action!\n" + 43 "Once an account is deleted, all the assets under this account may be lost!\n" + 44 "Type 'YES' to continue, quit for anything else.", 45 config.Chinese: "** 这是一个不可逆转的操作!\n" + 46 "一旦一个账户被删除, 该账户下的所有资源都可能会丢失!\n" + 47 "输入 'YES' 以继续, 否则退出", 48 } 49 _infoQuit = map[config.Language]string{ 50 config.English: "quit", 51 config.Chinese: "退出", 52 } 53 _flagGasLimitUsages = map[config.Language]string{ 54 config.English: "set gas limit", 55 config.Chinese: "设置燃气上限", 56 } 57 _flagGasPriceUsages = map[config.Language]string{ 58 config.English: `set gas price (unit: 10^(-6)IOTX), use suggested gas price if input is "0"`, 59 config.Chinese: `设置燃气费(单位:10^(-6)IOTX),如果输入为「0」,则使用默认燃气费`, 60 } 61 _flagNonceUsages = map[config.Language]string{ 62 config.English: "set nonce (default using pending nonce)", 63 config.Chinese: "设置 nonce (默认使用 pending nonce)", 64 } 65 _flagSignerUsages = map[config.Language]string{ 66 config.English: "choose a signing account", 67 config.Chinese: "选择要签名的帐户", 68 } 69 _flagBytecodeUsages = map[config.Language]string{ 70 config.English: "set the byte code", 71 config.Chinese: "设置字节码", 72 } 73 _flagAssumeYesUsages = map[config.Language]string{ 74 config.English: "answer yes for all confirmations", 75 config.Chinese: "为所有确认设置 yes", 76 } 77 _flagPasswordUsages = map[config.Language]string{ 78 config.English: "input password for account", 79 config.Chinese: "设置密码", 80 } 81 ) 82 83 // Flag label, short label and defaults 84 const ( 85 gasLimitFlagLabel = "gas-limit" 86 gasLimitFlagShortLabel = "l" 87 GasLimitFlagDefault = uint64(20000000) 88 gasPriceFlagLabel = "gas-price" 89 gasPriceFlagShortLabel = "p" 90 gasPriceFlagDefault = "1" 91 nonceFlagLabel = "nonce" 92 nonceFlagShortLabel = "n" 93 nonceFlagDefault = uint64(0) 94 signerFlagLabel = "signer" 95 signerFlagShortLabel = "s" 96 SignerFlagDefault = "" 97 bytecodeFlagLabel = "bytecode" 98 bytecodeFlagShortLabel = "b" 99 bytecodeFlagDefault = "" 100 assumeYesFlagLabel = "assume-yes" 101 assumeYesFlagShortLabel = "y" 102 assumeYesFlagDefault = false 103 passwordFlagLabel = "password" 104 passwordFlagShortLabel = "P" 105 passwordFlagDefault = "" 106 ) 107 108 func registerGasLimitFlag(client ioctl.Client, cmd *cobra.Command) { 109 flag.NewUint64VarP(gasLimitFlagLabel, gasLimitFlagShortLabel, GasLimitFlagDefault, selectTranslation(client, _flagGasLimitUsages)).RegisterCommand(cmd) 110 } 111 112 func registerGasPriceFlag(client ioctl.Client, cmd *cobra.Command) { 113 flag.NewStringVarP(gasPriceFlagLabel, gasPriceFlagShortLabel, gasPriceFlagDefault, selectTranslation(client, _flagGasPriceUsages)).RegisterCommand(cmd) 114 } 115 116 func registerNonceFlag(client ioctl.Client, cmd *cobra.Command) { 117 flag.NewUint64VarP(nonceFlagLabel, nonceFlagShortLabel, nonceFlagDefault, selectTranslation(client, _flagNonceUsages)).RegisterCommand(cmd) 118 } 119 120 func registerSignerFlag(client ioctl.Client, cmd *cobra.Command) { 121 flag.NewStringVarP(signerFlagLabel, signerFlagShortLabel, SignerFlagDefault, selectTranslation(client, _flagSignerUsages)).RegisterCommand(cmd) 122 } 123 124 func registerBytecodeFlag(client ioctl.Client, cmd *cobra.Command) { 125 flag.NewStringVarP(bytecodeFlagLabel, bytecodeFlagShortLabel, bytecodeFlagDefault, selectTranslation(client, _flagBytecodeUsages)).RegisterCommand(cmd) 126 } 127 128 func registerAssumeYesFlag(client ioctl.Client, cmd *cobra.Command) { 129 flag.BoolVarP(assumeYesFlagLabel, assumeYesFlagShortLabel, assumeYesFlagDefault, selectTranslation(client, _flagAssumeYesUsages)).RegisterCommand(cmd) 130 } 131 132 func registerPasswordFlag(client ioctl.Client, cmd *cobra.Command) { 133 flag.NewStringVarP(passwordFlagLabel, passwordFlagShortLabel, passwordFlagDefault, selectTranslation(client, _flagPasswordUsages)).RegisterCommand(cmd) 134 } 135 136 func selectTranslation(client ioctl.Client, trls map[config.Language]string) string { 137 txt, _ := client.SelectTranslation(trls) 138 return txt 139 } 140 141 // NewActionCmd represents the action command 142 func NewActionCmd(client ioctl.Client) *cobra.Command { 143 cmd := &cobra.Command{ 144 Use: "action", 145 Short: selectTranslation(client, _actionCmdShorts), 146 } 147 148 // TODO add sub commands 149 // cmd.AddCommand(NewActionHash(client)) 150 // cmd.AddCommand(NewActionTransfer(client)) 151 // cmd.AddCommand(NewActionDeploy(client)) 152 // cmd.AddCommand(NewActionInvoke(client)) 153 // cmd.AddCommand(NewActionRead(client)) 154 // cmd.AddCommand(NewActionClaim(client)) 155 // cmd.AddCommand(NewActionDeposit(client)) 156 // cmd.AddCommand(NewActionSendRaw(client)) 157 158 client.SetEndpointWithFlag(cmd.PersistentFlags().StringVar) 159 client.SetInsecureWithFlag(cmd.PersistentFlags().BoolVar) 160 161 return cmd 162 } 163 164 // RegisterWriteCommand registers action flags for command 165 func RegisterWriteCommand(client ioctl.Client, cmd *cobra.Command) { 166 registerGasLimitFlag(client, cmd) 167 registerGasPriceFlag(client, cmd) 168 registerSignerFlag(client, cmd) 169 registerNonceFlag(client, cmd) 170 registerAssumeYesFlag(client, cmd) 171 registerPasswordFlag(client, cmd) 172 } 173 174 // GetWriteCommandFlag returns action flags for command 175 func GetWriteCommandFlag(cmd *cobra.Command) (gasPrice, signer, password string, nonce, gasLimit uint64, assumeYes bool, err error) { 176 gasPrice, err = cmd.Flags().GetString(gasPriceFlagLabel) 177 if err != nil { 178 err = errors.Wrap(err, "failed to get flag gas-price") 179 return 180 } 181 signer, err = cmd.Flags().GetString(signerFlagLabel) 182 if err != nil { 183 err = errors.Wrap(err, "failed to get flag signer") 184 return 185 } 186 password, err = cmd.Flags().GetString(passwordFlagLabel) 187 if err != nil { 188 err = errors.Wrap(err, "failed to get flag password") 189 return 190 } 191 nonce, err = cmd.Flags().GetUint64(nonceFlagLabel) 192 if err != nil { 193 err = errors.Wrap(err, "failed to get flag nonce") 194 return 195 } 196 gasLimit, err = cmd.Flags().GetUint64(gasLimitFlagLabel) 197 if err != nil { 198 err = errors.Wrap(err, "failed to get flag gas-limit") 199 return 200 } 201 assumeYes, err = cmd.Flags().GetBool(assumeYesFlagLabel) 202 if err != nil { 203 err = errors.Wrap(err, "failed to get flag assume-yes") 204 return 205 } 206 return 207 } 208 209 func handleClientRequestError(err error, apiName string) error { 210 if sta, ok := status.FromError(err); ok { 211 if sta.Code() == codes.Unavailable { 212 return ioctl.ErrInvalidEndpointOrInsecure 213 } 214 return errors.New(sta.Message()) 215 } 216 return errors.Wrapf(err, "failed to invoke %s api", apiName) 217 } 218 219 // Signer returns signer's address 220 func Signer(client ioctl.Client, signer string) (string, error) { 221 if util.AliasIsHdwalletKey(signer) { 222 return signer, nil 223 } 224 return client.AddressWithDefaultIfNotExist(signer) 225 } 226 227 func checkNonce(client ioctl.Client, nonce uint64, executor string) (uint64, error) { 228 if util.AliasIsHdwalletKey(executor) { 229 // for hdwallet key, get the nonce in SendAction() 230 return 0, nil 231 } 232 if nonce != 0 { 233 return nonce, nil 234 } 235 accountMeta, err := account.Meta(client, executor) 236 if err != nil { 237 return 0, errors.Wrap(err, "failed to get account meta") 238 } 239 return accountMeta.PendingNonce, nil 240 } 241 242 // gasPriceInRau returns the suggest gas price 243 func gasPriceInRau(client ioctl.Client, gasPrice string) (*big.Int, error) { 244 if client.IsCryptoSm2() { 245 return big.NewInt(0), nil 246 } 247 if len(gasPrice) != 0 { 248 return util.StringToRau(gasPrice, util.GasPriceDecimalNum) 249 } 250 251 cli, err := client.APIServiceClient() 252 if err != nil { 253 return nil, errors.Wrap(err, "failed to connect to endpoint") 254 } 255 256 ctx := context.Background() 257 if jwtMD, err := util.JwtAuth(); err == nil { 258 ctx = metautils.NiceMD(jwtMD).ToOutgoing(ctx) 259 } 260 261 rsp, err := cli.SuggestGasPrice(ctx, &iotexapi.SuggestGasPriceRequest{}) 262 if err != nil { 263 return nil, handleClientRequestError(err, "SuggestGasPrice") 264 } 265 return new(big.Int).SetUint64(rsp.GasPrice), nil 266 } 267 268 func fixGasLimit(client ioctl.Client, caller string, execution *action.Execution) (*action.Execution, error) { 269 cli, err := client.APIServiceClient() 270 if err != nil { 271 return nil, errors.Wrap(err, "failed to connect to endpoint") 272 } 273 274 ctx := context.Background() 275 if jwtMD, err := util.JwtAuth(); err == nil { 276 ctx = metautils.NiceMD(jwtMD).ToOutgoing(ctx) 277 } 278 279 res, err := cli.EstimateActionGasConsumption(ctx, 280 &iotexapi.EstimateActionGasConsumptionRequest{ 281 Action: &iotexapi.EstimateActionGasConsumptionRequest_Execution{ 282 Execution: execution.Proto(), 283 }, 284 CallerAddress: caller, 285 }) 286 if err != nil { 287 return nil, handleClientRequestError(err, "EstimateActionGasConsumption") 288 } 289 return action.NewExecution(execution.Contract(), execution.Nonce(), execution.Amount(), res.Gas, execution.GasPrice(), execution.Data()) 290 } 291 292 // SendRaw sends raw action to blockchain 293 func SendRaw(client ioctl.Client, cmd *cobra.Command, selp *iotextypes.Action) error { 294 cli, err := client.APIServiceClient() 295 if err != nil { 296 return errors.Wrap(err, "failed to connect to endpoint") 297 } 298 299 ctx := context.Background() 300 if jwtMD, err := util.JwtAuth(); err == nil { 301 ctx = metautils.NiceMD(jwtMD).ToOutgoing(ctx) 302 } 303 304 _, err = cli.SendAction(ctx, &iotexapi.SendActionRequest{Action: selp}) 305 if err != nil { 306 return handleClientRequestError(err, "SendAction") 307 } 308 309 shash := hash.Hash256b(byteutil.Must(proto.Marshal(selp))) 310 txhash := hex.EncodeToString(shash[:]) 311 URL := "https://" 312 endpoint := client.Config().Endpoint 313 explorer := client.Config().Explorer 314 switch explorer { 315 case "iotexscan": 316 if strings.Contains(endpoint, "testnet") { 317 URL += "testnet." 318 } 319 URL += "iotexscan.io/action/" + txhash 320 case "iotxplorer": 321 URL = "iotxplorer.io/actions/" + txhash 322 default: 323 URL = explorer + txhash 324 } 325 cmd.Printf("Action has been sent to blockchain.\nWait for several seconds and query this action by hash: %s\n", URL) 326 return nil 327 } 328 329 // SendAction sends signed action to blockchain 330 func SendAction(client ioctl.Client, 331 cmd *cobra.Command, 332 elp action.Envelope, 333 signer, password string, 334 nonce uint64, 335 assumeYes bool, 336 ) error { 337 sk, err := account.PrivateKeyFromSigner(client, cmd, signer, password) 338 if err != nil { 339 return errors.Wrap(err, "failed to get privateKey") 340 } 341 342 chainMeta, err := bc.GetChainMeta(client) 343 if err != nil { 344 return errors.Wrap(err, "failed to get chain meta") 345 } 346 elp.SetChainID(chainMeta.GetChainID()) 347 348 if util.AliasIsHdwalletKey(signer) { 349 addr := sk.PublicKey().Address() 350 signer = addr.String() 351 nonce, err = checkNonce(client, nonce, signer) 352 if err != nil { 353 return errors.Wrap(err, "failed to get nonce") 354 } 355 elp.SetNonce(nonce) 356 } 357 358 sealed, err := action.Sign(elp, sk) 359 if err != nil { 360 return errors.Wrap(err, "failed to sign action") 361 } 362 if err := isBalanceEnough(client, signer, sealed); err != nil { 363 return errors.Wrap(err, "failed to pass balance check") 364 } 365 366 selp := sealed.Proto() 367 sk.Zero() 368 actionInfo, err := printActionProto(client, selp) 369 if err != nil { 370 return errors.Wrap(err, "failed to print action proto message") 371 } 372 cmd.Println(actionInfo) 373 374 if !assumeYes { 375 infoWarn := selectTranslation(client, _infoWarn) 376 infoQuit := selectTranslation(client, _infoQuit) 377 confirmed, err := client.AskToConfirm(infoWarn) 378 if err != nil { 379 return errors.Wrap(err, "failed to ask confirm") 380 } 381 if !confirmed { 382 cmd.Println(infoQuit) 383 return nil 384 } 385 } 386 387 return SendRaw(client, cmd, selp) 388 } 389 390 // Execute sends signed execution's transaction to blockchain 391 func Execute(client ioctl.Client, 392 cmd *cobra.Command, 393 contract string, 394 amount *big.Int, 395 bytecode []byte, 396 gasPrice, signer, password string, 397 nonce, gasLimit uint64, 398 assumeYes bool, 399 ) error { 400 if len(contract) == 0 && len(bytecode) == 0 { 401 return errors.New("failed to deploy contract with empty bytecode") 402 } 403 gasPriceRau, err := gasPriceInRau(client, gasPrice) 404 if err != nil { 405 return errors.Wrap(err, "failed to get gas price") 406 } 407 sender, err := Signer(client, signer) 408 if err != nil { 409 return errors.Wrap(err, "failed to get signer address") 410 } 411 nonce, err = checkNonce(client, nonce, sender) 412 if err != nil { 413 return errors.Wrap(err, "failed to get nonce") 414 } 415 tx, err := action.NewExecution(contract, nonce, amount, gasLimit, gasPriceRau, bytecode) 416 if err != nil || tx == nil { 417 return errors.Wrap(err, "failed to make a Execution instance") 418 } 419 if gasLimit == 0 { 420 tx, err = fixGasLimit(client, sender, tx) 421 if err != nil || tx == nil { 422 return errors.Wrap(err, "failed to fix Execution gas limit") 423 } 424 gasLimit = tx.GasLimit() 425 } 426 return SendAction( 427 client, 428 cmd, 429 (&action.EnvelopeBuilder{}). 430 SetNonce(nonce). 431 SetGasPrice(gasPriceRau). 432 SetGasLimit(gasLimit). 433 SetAction(tx).Build(), 434 sender, 435 password, 436 nonce, 437 assumeYes, 438 ) 439 } 440 441 // Read reads smart contract on IoTeX blockchain 442 func Read(client ioctl.Client, 443 contract address.Address, 444 amount string, 445 bytecode []byte, 446 signer string, 447 gasLimit uint64, 448 ) (string, error) { 449 cli, err := client.APIServiceClient() 450 if err != nil { 451 return "", errors.Wrap(err, "failed to connect to endpoint") 452 } 453 454 ctx := context.Background() 455 if jwtMD, err := util.JwtAuth(); err == nil { 456 ctx = metautils.NiceMD(jwtMD).ToOutgoing(ctx) 457 } 458 459 callerAddr, err := Signer(client, signer) 460 if err != nil { 461 return "", errors.Wrap(err, "failed to get signer address") 462 } 463 if callerAddr == "" { 464 callerAddr = address.ZeroAddress 465 } 466 467 res, err := cli.ReadContract(ctx, 468 &iotexapi.ReadContractRequest{ 469 Execution: &iotextypes.Execution{ 470 Amount: amount, 471 Contract: contract.String(), 472 Data: bytecode, 473 }, 474 CallerAddress: callerAddr, 475 GasLimit: gasLimit, 476 }, 477 ) 478 if err != nil { 479 return "", handleClientRequestError(err, "ReadContract") 480 } 481 return res.Data, nil 482 } 483 484 func isBalanceEnough(client ioctl.Client, address string, act *action.SealedEnvelope) error { 485 accountMeta, err := account.Meta(client, address) 486 if err != nil { 487 return errors.Wrap(err, "failed to get account meta") 488 } 489 balance, ok := new(big.Int).SetString(accountMeta.Balance, 10) 490 if !ok { 491 return errors.New("failed to convert balance into big int") 492 } 493 cost, err := act.Cost() 494 if err != nil { 495 return errors.Wrap(err, "failed to check cost of an action") 496 } 497 if balance.Cmp(cost) < 0 { 498 return errors.New("balance is not enough") 499 } 500 return nil 501 }