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  }