aergoio/aergo

View on GitHub
cmd/aergocli/cmd/contract.go

Summary

Maintainability
D
2 days
Test Coverage
F
19%
package cmd

import (
    "bytes"
    "context"
    "encoding/json"
    "errors"
    "fmt"
    "os"
    "strconv"

    luacEncoding "github.com/aergoio/aergo/v2/cmd/aergoluac/encoding"
    luac "github.com/aergoio/aergo/v2/cmd/aergoluac/util"
    "github.com/aergoio/aergo/v2/internal/common"
    "github.com/aergoio/aergo/v2/internal/enc/base58"
    "github.com/aergoio/aergo/v2/internal/enc/hex"
    "github.com/aergoio/aergo/v2/types"
    aergorpc "github.com/aergoio/aergo/v2/types"
    "github.com/aergoio/aergo/v2/types/jsonrpc"
    "github.com/spf13/cobra"
)

var (
    client        *ConnClient
    admClient     types.AdminRPCServiceClient
    data          string
    nonce         uint64
    toJSON        bool
    gover         bool
    feeDelegation bool
    contractID    string
    gas           uint64
)

func intListToString(ns []int, word string) string {
    if ns == nil || len(ns) == 0 {
        return ""
    }
    if len(ns) == 1 {
        return strconv.Itoa(ns[0])
    }
    slice := ns[:len(ns)-1]
    end := strconv.Itoa(ns[len(ns)-1])
    ret := ""
    for idx, n := range slice {
        ret += strconv.Itoa(n)
        if idx < len(ns)-2 {
            ret += ", "
        }
    }
    return fmt.Sprintf("%s %s %s", ret, word, end)
}

func nArgs(ns []int) cobra.PositionalArgs {
    return func(cmd *cobra.Command, args []string) error {
        for _, n := range ns {
            if n == len(args) {
                return nil
            }
        }
        return fmt.Errorf("requires exactly %s args but received %d", intListToString(ns, "or"), len(args))
    }
}

func init() {
    contractCmd := &cobra.Command{
        Use:   "contract [flags] subcommand",
        Short: "Contract command",
    }
    contractCmd.PersistentFlags().Uint64VarP(&gas, "gaslimit", "g", 0, "Gas limit")

    deployCmd := &cobra.Command{
        Use: `deploy [flags] --payload 'payload string' <creatorAddress> [args]
  aergocli contract deploy [flags] <creatorAddress> <bcfile> <abifile> [args]
  
  You can pass constructor arguments by passing a JSON string as the optional final parameter, e.g. "[1, 2, 3]".`,
        Short:                 "Deploy a compiled contract to the server",
        Args:                  nArgs([]int{1, 2, 3, 4}),
        RunE:                  runDeployCmd,
        DisableFlagsInUseLine: true,
    }
    deployCmd.PersistentFlags().Uint64Var(&nonce, "nonce", 0, "manually set a nonce (default: set nonce automatically)")
    deployCmd.PersistentFlags().StringVar(&data, "payload", "", "result of compiling a contract")
    deployCmd.PersistentFlags().StringVar(&amount, "amount", "0", "amount of token to send with deployment, in aer")
    deployCmd.PersistentFlags().StringVarP(&contractID, "redeploy", "r", "", "redeploy the contract")
    deployCmd.Flags().StringVar(&pw, "password", "", "password (optional, will be asked on the terminal if not given)")

    callCmd := &cobra.Command{
        Use: `call [flags] <sender> <contract> <funcname> [args]

  You can pass function arguments by passing a JSON string as the optional final parameter, e.g. "[1, 2, 3]".`,
        Short: "Call a contract function",
        Args:  nArgs([]int{3, 4}),
        RunE:  runCallCmd,
    }
    callCmd.PersistentFlags().Uint64Var(&nonce, "nonce", 0, "manually set a nonce (default: set nonce automatically)")
    callCmd.PersistentFlags().StringVar(&amount, "amount", "0", "amount of token to send with call, in aer")
    callCmd.PersistentFlags().StringVar(&chainIdHash, "chainidhash", "", "chain id hash value encoded by base58")
    callCmd.PersistentFlags().BoolVar(&toJSON, "tojson", false, "display json transaction instead of sending to blockchain")
    callCmd.PersistentFlags().BoolVar(&gover, "governance", false, "setting type")
    callCmd.PersistentFlags().BoolVar(&feeDelegation, "delegation", false, "request fee delegation to contract")
    callCmd.Flags().StringVar(&pw, "password", "", "password (optional, will be asked on the terminal if not given)")

    stateQueryCmd := &cobra.Command{
        Use:   "statequery [flags] <contractAddress> <varname> [varindex]",
        Short: "query the state of a contract with variable name and optional index",
        Args:  cobra.MinimumNArgs(2),
        RunE:  runQueryStateCmd,
    }
    stateQueryCmd.Flags().StringVar(&stateroot, "root", "", "Query the state at a specified state root")
    stateQueryCmd.Flags().BoolVar(&compressed, "compressed", false, "Get a compressed proof for the state")

    contractCmd.AddCommand(
        deployCmd,
        callCmd,
        &cobra.Command{
            Use:   "abi [flags] <contractAddress>",
            Short: "Get ABI of the contract",
            Args:  cobra.ExactArgs(1),
            RunE:  runGetABICmd,
        },
        &cobra.Command{
            Use:   "query [flags] <contractAddress> <funcname> [args]",
            Short: "Query contract by executing read-only function",
            Args:  cobra.MinimumNArgs(2),
            RunE:  runQueryCmd,
        },
        stateQueryCmd,
    )
    rootCmd.AddCommand(contractCmd)
}

func isHexString(s string) bool {
    // check is the input has even number of characters
    if len(s)%2 != 0 {
        return false
    }
    // check if the input contains only hex characters
    for _, c := range s {
        if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) {
            return false
        }
    }
    return true
}

func runDeployCmd(cmd *cobra.Command, args []string) error {
    var err error
    var code []byte
    var deployArgs []byte

    cmd.SilenceUsage = true

    creator, err := types.DecodeAddress(args[0])
    if err != nil {
        return fmt.Errorf("could not decode address: %v", err.Error())
    }

    if nonce == 0 {
        state, err := client.GetState(context.Background(), &types.SingleBytes{Value: creator})
        if err != nil {
            return fmt.Errorf("failed to get creator account's state: %v", err.Error())
        }
        nonce = state.GetNonce() + 1
    }

    var payload []byte
    if len(data) == 0 {
        if len(args) < 3 {
            cmd.SilenceUsage = false
            return errors.New("not enough arguments")
        }
        code, err = os.ReadFile(args[1])
        if err != nil {
            return fmt.Errorf("failed to read code file: %v", err.Error())
        }
        var abi []byte
        abi, err = os.ReadFile(args[2])
        if err != nil {
            return fmt.Errorf("failed to read abi file: %v", err.Error())
        }
        if len(args) == 4 {
            var ci types.CallInfo
            err = json.Unmarshal([]byte(args[3]), &ci.Args)
            if err != nil {
                return fmt.Errorf("failed to parse JSON: %v", err.Error())
            }
            deployArgs = []byte(args[3])
        }
        payload = luac.NewLuaCodePayload(luac.NewLuaCode(code, abi), deployArgs)
    } else {
        if len(args) == 2 {
            var ci types.CallInfo
            err = json.Unmarshal([]byte(args[1]), &ci.Args)
            if err != nil {
                return fmt.Errorf("failed to parse JSON: %v", err.Error())
            }
            deployArgs = []byte(args[1])
        }
        // check if the data is in hex format
        if isHexString(data) {
            // the data is expected to be copied from aergoscan view of
            // the transaction that deployed the contract
            payload, err = hex.Decode(data)
        } else {
            // the data is the output of aergoluac
            code, err = luacEncoding.DecodeCode(data)
            if err != nil {
                return fmt.Errorf("failed to decode code: %v", err.Error())
            }
            payload = luac.NewLuaCodePayload(luac.LuaCode(code), deployArgs)
        }
    }

    amountBigInt, err := jsonrpc.ParseUnit(amount)
    if err != nil {
        return fmt.Errorf("failed to parse amount: %v", err.Error())
    }

    txType := types.TxType_DEPLOY
    var recipient []byte
    if len(contractID) > 0 {
        txType = types.TxType_REDEPLOY
        recipient, err = types.DecodeAddress(contractID)
        if err != nil {
            return fmt.Errorf("failed to decode contract address: %v", err.Error())
        }
    }

    tx := &types.Tx{
        Body: &types.TxBody{
            Nonce:     nonce,
            Account:   creator,
            Payload:   payload,
            Amount:    amountBigInt.Bytes(),
            GasLimit:  gas,
            Type:      txType,
            Recipient: recipient,
        },
    }
    cmd.Println(sendTX(cmd, tx, creator))
    return nil
}

func runCallCmd(cmd *cobra.Command, args []string) error {
    cmd.SilenceUsage = true

    caller, err := types.DecodeAddress(args[0])
    if err != nil {
        return fmt.Errorf("could not decode sender address: %v", err.Error())
    }
    if nonce == 0 {
        state, err := client.GetState(context.Background(), &types.SingleBytes{Value: caller})
        if err != nil {
            return fmt.Errorf("failed to get creator account's state: %v", err.Error())
        }
        nonce = state.GetNonce() + 1
    }
    contract, err := types.DecodeAddress(args[1])
    if err != nil {
        return fmt.Errorf("could not decode contract address: %v", err.Error())
    }

    var ci types.CallInfo
    ci.Name = args[2]
    if len(args) > 3 {
        err = json.Unmarshal([]byte(args[3]), &ci.Args)
        if err != nil {
            return fmt.Errorf("failed to parse JSON: %v", err.Error())
        }
    }
    payload, err := json.Marshal(ci)
    if err != nil {
        return fmt.Errorf("failed to encode JSON: %v", err.Error())
    }

    if !toJSON && !gover {
        abi, err := client.GetABI(context.Background(), &types.SingleBytes{Value: contract})
        if err != nil {
            return fmt.Errorf("failed to get abi: %v", err.Error())
        }
        if !abi.HasFunction(args[2]) {
            return fmt.Errorf("function %v not found in contract at address %s", args[2], args[1])
        }
    }

    amountBigInt, err := jsonrpc.ParseUnit(amount)
    if err != nil {
        return fmt.Errorf("failed to parse amount: %v", err)
    }

    var txType types.TxType
    if gover {
        txType = types.TxType_GOVERNANCE
    } else if feeDelegation {
        txType = types.TxType_FEEDELEGATION
    } else {
        txType = types.TxType_CALL
    }

    tx := &types.Tx{
        Body: &types.TxBody{
            Nonce:     nonce,
            Account:   caller,
            Recipient: contract,
            Payload:   payload,
            Amount:    amountBigInt.Bytes(),
            GasLimit:  gas,
            Type:      txType,
        },
    }

    if chainIdHash != "" {
        rawCidHash, err := base58.Decode(chainIdHash)
        if err != nil {
            return fmt.Errorf("failed to parse chainidhash: %v", err.Error())
        }
        tx.Body.ChainIdHash = rawCidHash
    } else {
        if errStr := fillChainId(tx); errStr != "" {
            return errors.New(errStr)
        }
    }

    if pw == "" {
        pw, err = getPasswd(cmd, false)
        if err != nil {
            return err
        }
    }

    if rootConfig.KeyStorePath != "" {
        if errStr := fillSign(tx, rootConfig.KeyStorePath, pw, caller); errStr != "" {
            return errors.New(errStr)
        }
    } else {
        sign, err := client.SignTX(context.Background(), tx)
        if err != nil || sign == nil {
            return fmt.Errorf("failed to sign tx: %v", err)
        }
        tx = sign
    }

    if toJSON {
        res := jsonrpc.ConvTx(tx, jsonrpc.Base58)
        cmd.Println(jsonrpc.MarshalJSON(res))
    } else {
        txs := []*types.Tx{tx}
        var msgs *types.CommitResultList
        msgs, err = client.CommitTX(context.Background(), &types.TxList{Txs: txs})
        if err != nil {
            return fmt.Errorf("failed to commit tx: %v", err.Error())
        }
        res := jsonrpc.ConvCommitResult(msgs.Results[0])
        cmd.Println(jsonrpc.MarshalJSON(res))
    }
    return nil
}

func runGetABICmd(cmd *cobra.Command, args []string) error {
    cmd.SilenceUsage = true

    contract, err := types.DecodeAddress(args[0])
    if err != nil {
        return fmt.Errorf("failed to decode address: %v", err.Error())
    }
    abi, err := client.GetABI(context.Background(), &types.SingleBytes{Value: contract})
    if err != nil {
        return fmt.Errorf("failed to get abi: %v", err.Error())
    }
    res := jsonrpc.ConvAbi(abi)
    cmd.Println(jsonrpc.MarshalJSON(res))
    return nil
}

func runQueryCmd(cmd *cobra.Command, args []string) error {
    cmd.SilenceUsage = true

    contract, err := types.DecodeAddress(args[0])
    if err != nil {
        return fmt.Errorf("failed to decode address: %v", err.Error())
    }
    var ci types.CallInfo

    ci.Name = args[1]
    if len(args) > 2 {
        err = json.Unmarshal([]byte(args[2]), &ci.Args)
        if err != nil {
            return fmt.Errorf("failed to parse JSON: %v", err.Error())
        }
    }
    callinfo, err := json.Marshal(ci)
    if err != nil {
        return fmt.Errorf("failed to encode JSON: %v", err.Error())
    }

    query := &types.Query{
        ContractAddress: contract,
        Queryinfo:       callinfo,
    }

    ret, err := client.QueryContract(context.Background(), query)
    if err != nil {
        return fmt.Errorf("failed to query contract: %v", err.Error())
    }
    cmd.Println(ret)
    return nil
}

func runQueryStateCmd(cmd *cobra.Command, args []string) error {
    cmd.SilenceUsage = true

    var root []byte
    var err error
    contract, err := types.DecodeAddress(args[0])
    if err != nil {
        return fmt.Errorf("failed to decode address: %v", err.Error())
    }
    if len(stateroot) != 0 {
        root, err = base58.Decode(stateroot)
        if err != nil {
            return fmt.Errorf("failed to decode stateroot: %v", err.Error())
        }
    }
    storageKeyPlain := bytes.NewBufferString("_sv_")
    storageKeyPlain.WriteString(args[1])
    if len(args) > 2 {
        storageKeyPlain.WriteString("-")
        storageKeyPlain.WriteString(args[2])
    }
    storageKey := common.Hasher([]byte(storageKeyPlain.Bytes()))
    stateQuery := &types.StateQuery{
        ContractAddress: contract,
        StorageKeys:     [][]byte{storageKey},
        Root:            root,
        Compressed:      compressed,
    }
    ret, err := client.QueryContractState(context.Background(), stateQuery)
    if err != nil {
        return fmt.Errorf("failed to query contract state: %v", err.Error())
    }
    cmd.Println(ret)
    return nil
}

func fillChainId(tx *types.Tx) string {
    msg, err := client.Blockchain(context.Background(), &aergorpc.Empty{})
    if err != nil {
        return fmt.Sprintf("Failed: %s\n", err.Error())
    }
    tx.Body.ChainIdHash = msg.GetBestChainIdHash()
    return ""
}