Skip to content

Commit

Permalink
Allow ioctl to show the list of actions for an account (#2750)
Browse files Browse the repository at this point in the history
* [ioctl] allow ioctl to show the list of actions for an account
  • Loading branch information
Liuhaai committed Sep 17, 2021
1 parent d10dabe commit 44e0a68
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 149 deletions.
181 changes: 53 additions & 128 deletions ioctl/cmd/account/accountactions.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,37 @@
package account

import (
"context"
"bytes"
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"

"github.com/golang/protobuf/ptypes"
"github.com/grpc-ecosystem/go-grpc-middleware/util/metautils"
"github.com/rodaine/table"
"github.com/spf13/cobra"
"google.golang.org/grpc/status"

"github.com/iotexproject/iotex-core/ioctl/config"
"github.com/iotexproject/iotex-core/ioctl/output"
"github.com/iotexproject/iotex-core/ioctl/util"
"github.com/iotexproject/iotex-proto/golang/iotexapi"
"github.com/iotexproject/iotex-proto/golang/iotextypes"
)

var countLimit *uint64
type (
allActionsByAddressResult struct {
ActHash string
BlkHeight string
Sender string
Recipient string
ActType string
Amount string
TimeStamp string
RecordType string
}

allActionsByAddressResponse struct {
Count string
Results []*allActionsByAddressResult
}
)

// Multi-language support
var (
Expand All @@ -37,10 +49,6 @@ var (
config.English: "actions (ALIAS|ADDRESS) [SKIP]",
config.Chinese: "actions (ALIAS|ADDRESS) [SKIP]",
}
flagCountUsages = map[config.Language]string{
config.English: "choose a count limit",
config.Chinese: "选择一个计数限制",
}
)

// accountActionsCmd represents the account sign command
Expand All @@ -55,12 +63,8 @@ var accountActionsCmd = &cobra.Command{
},
}

func init() {
countLimit = accountActionsCmd.Flags().Uint64("limit", 15, config.TranslateInLang(flagCountUsages, config.UILanguage))
}

func accountActions(args []string) error {
var skip uint64
var skip uint64 = 0
var err error
if len(args) == 2 {
skip, err = strconv.ParseUint(args[1], 10, 64)
Expand All @@ -73,130 +77,51 @@ func accountActions(args []string) error {
if err != nil {
return output.NewError(output.AddressError, "failed to get address", err)
}

conn, err := util.ConnectToEndpoint(config.ReadConfig.SecureConnect && !config.Insecure)
if err != nil {
return output.NewError(output.NetworkError, "failed to connect to endpoint", err)
}
defer conn.Close()
cli := iotexapi.NewAPIServiceClient(conn)
ctx := context.Background()

jwtMD, err := util.JwtAuth()
if err == nil {
ctx = metautils.NiceMD(jwtMD).ToOutgoing(ctx)
reqData := map[string]string{
"address": addr,
"offset": fmt.Sprint(skip),
}

requestGetAccount := iotexapi.GetAccountRequest{
Address: addr,
}
accountResponse, err := cli.GetAccount(ctx, &requestGetAccount)
jsonData, err := json.Marshal(reqData)
if err != nil {
return output.NewError(output.APIError, "failed to get account", err)
}
numActions := accountResponse.AccountMeta.GetNumActions()
fmt.Println("Total:", numActions)
requestGetAction := iotexapi.GetActionsRequest{
Lookup: &iotexapi.GetActionsRequest_ByAddr{
ByAddr: &iotexapi.GetActionsByAddressRequest{
Address: addr,
Start: numActions - *countLimit - skip,
Count: *countLimit,
},
},
return output.NewError(output.ConvertError, "failed to pack in json", nil)
}
response, err := cli.GetActions(ctx, &requestGetAction)
resp, err := http.Post(config.ReadConfig.AnalyserEndpoint+"/api.ActionsService.GetActionsByAddress", "application/json",
bytes.NewBuffer(jsonData))
if err != nil {
sta, ok := status.FromError(err)
if ok {
return output.NewError(output.APIError, sta.Message(), nil)
}
return output.NewError(output.NetworkError, "failed to invoke GetActions api", err)
return output.NewError(output.NetworkError, "failed to send request", nil)
}
if len(response.ActionInfo) == 0 {
return output.NewError(output.APIError, "no action info returned", nil)

var respData allActionsByAddressResponse
err = json.NewDecoder(resp.Body).Decode(&respData)
if err != nil {
return output.NewError(output.SerializationError, "failed to deserialize the response", nil)
}
actions := respData.Results

fmt.Println("Total:", len(actions))
showFields := []interface{}{
"Hash",
"Time",
"Status",
"ActHash",
"TimeStamp",
"BlkHeight",
"ActCategory",
"ActType",
"Sender",
"Type",
"To",
"Contract",
"Recipient",
"Amount",
}
tbl := table.New(showFields...)

for i := range response.ActionInfo {
k := len(response.ActionInfo) - 1 - i
actionInfo := response.ActionInfo[k]
requestGetReceipt := &iotexapi.GetReceiptByActionRequest{ActionHash: actionInfo.ActHash}
responseReceipt, err := cli.GetReceiptByAction(ctx, requestGetReceipt)
if err != nil {
sta, ok := status.FromError(err)
if ok {
fmt.Println(output.NewError(output.APIError, sta.Message(), nil))
} else {
fmt.Println(output.NewError(output.NetworkError, "failed to invoke GetReceiptByAction api", err))
}
continue
}
amount := "0"
transfer := actionInfo.Action.Core.GetTransfer()
if transfer != nil {
amount, _ = util.StringToIOTX(transfer.Amount)

}
tbl.AddRow(
tb := table.New(showFields...)
for _, actionInfo := range actions {
tb.AddRow(
actionInfo.ActHash,
getActionTime(actionInfo),
iotextypes.ReceiptStatus_name[int32(responseReceipt.ReceiptInfo.Receipt.GetStatus())],
actionInfo.TimeStamp,
actionInfo.BlkHeight,
actionInfo.RecordType,
actionInfo.ActType,
actionInfo.Sender,
getActionTypeString(actionInfo),
getActionTo(actionInfo),
getActionContract(responseReceipt),
amount+" IOTX",
actionInfo.Recipient,
actionInfo.Amount+" IOTX",
)
}
tbl.Print()
tb.Print()
return nil
}

func getActionContract(responseReceipt *iotexapi.GetReceiptByActionResponse) string {
contract := responseReceipt.ReceiptInfo.Receipt.GetContractAddress()
if contract == "" {
contract = "-"
}
return contract
}

func getActionTypeString(actionInfo *iotexapi.ActionInfo) string {
actionType := fmt.Sprintf("%T", actionInfo.Action.Core.GetAction())
return strings.TrimLeft(actionType, "*iotextypes.ActionCore_")
}

func getActionTo(actionInfo *iotexapi.ActionInfo) string {
recipient := ""
switch getActionTypeString(actionInfo) {
case "Transfer":
transfer := actionInfo.Action.Core.GetTransfer()
recipient = transfer.GetRecipient()
case "Execution":
execution := actionInfo.Action.Core.GetExecution()
recipient = execution.GetContract()
}
if recipient == "" {
recipient = "-"
}
return recipient
}

func getActionTime(actionInfo *iotexapi.ActionInfo) string {
if actionInfo.Timestamp != nil {
if ts, err := ptypes.Timestamp(actionInfo.Timestamp); err == nil {
return ts.String()
}
}
return ""
}
21 changes: 13 additions & 8 deletions ioctl/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,15 @@ type Context struct {

// Config defines the config schema
type Config struct {
Wallet string `json:"wallet" yaml:"wallet"`
Endpoint string `json:"endpoint" yaml:"endpoint"`
SecureConnect bool `json:"secureConnect" yaml:"secureConnect"`
Aliases map[string]string `json:"aliases" yaml:"aliases"`
DefaultAccount Context `json:"defaultAccount" yaml:"defaultAccount"`
Explorer string `json:"explorer" yaml:"explorer"`
Language string `json:"language" yaml:"language"`
Nsv2height uint64 `json:"nsv2height" yaml:"nsv2height"`
Wallet string `json:"wallet" yaml:"wallet"`
Endpoint string `json:"endpoint" yaml:"endpoint"`
SecureConnect bool `json:"secureConnect" yaml:"secureConnect"`
Aliases map[string]string `json:"aliases" yaml:"aliases"`
DefaultAccount Context `json:"defaultAccount" yaml:"defaultAccount"`
Explorer string `json:"explorer" yaml:"explorer"`
Language string `json:"language" yaml:"language"`
Nsv2height uint64 `json:"nsv2height" yaml:"nsv2height"`
AnalyserEndpoint string `json:"analyserEndpoint" yaml:"analyserEndpoint"`
}

var (
Expand Down Expand Up @@ -108,6 +109,10 @@ func init() {
if ReadConfig.Nsv2height == 0 {
ReadConfig.Nsv2height = config.Default.Genesis.FairbankBlockHeight
}
if ReadConfig.AnalyserEndpoint == "" {
ReadConfig.AnalyserEndpoint = defaultAnalyserEndpoint
completeness = false
}
if !completeness {
err := writeConfig()
if err != nil {
Expand Down
26 changes: 13 additions & 13 deletions ioctl/config/configsetget.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,18 @@ import (

// Regexp patterns
const (
ipPattern = `((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)`
domainPattern = `[a-zA-Z0-9][a-zA-Z0-9_-]{0,62}(\.[a-zA-Z0-9][a-zA-Z0-9_-]{0,62})*(\.[a-zA-Z][a-zA-Z0-9]{0,10}){1}`
urlPattern = `[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)`
localPattern = "localhost"
endpointPattern = "(" + ipPattern + "|(" + domainPattern + ")" + "|(" + localPattern + "))" + `(:\d{1,5})?`
ipPattern = `((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)`
domainPattern = `[a-zA-Z0-9][a-zA-Z0-9_-]{0,62}(\.[a-zA-Z0-9][a-zA-Z0-9_-]{0,62})*(\.[a-zA-Z][a-zA-Z0-9]{0,10}){1}`
urlPattern = `[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)`
localPattern = "localhost"
endpointPattern = "(" + ipPattern + "|(" + domainPattern + ")" + "|(" + localPattern + "))" + `(:\d{1,5})?`
defaultAnalyserEndpoint = "https://iotex-analyser-api-mainnet.chainanalytics.org"
)

var (
supportedLanguage = []string{"English", "中文"}
validArgs = []string{"endpoint", "wallet", "explorer", "defaultacc", "language", "nsv2height"}
validGetArgs = []string{"endpoint", "wallet", "explorer", "defaultacc", "language", "nsv2height", "all"}
validGetArgs = []string{"endpoint", "wallet", "explorer", "defaultacc", "language", "nsv2height", "analyserEndpoint", "all"}
validExpl = []string{"iotexscan", "iotxplorer"}
endpointCompile = regexp.MustCompile("^" + endpointPattern + "$")
)
Expand Down Expand Up @@ -133,29 +134,25 @@ func Get(arg string) error {
}
message := endpointMessage{Endpoint: ReadConfig.Endpoint, SecureConnect: ReadConfig.SecureConnect}
fmt.Println(message.String())
return nil
case "wallet":
output.PrintResult(ReadConfig.Wallet)
return nil
case "defaultacc":
if ReadConfig.DefaultAccount.AddressOrAlias == "" {
return output.NewError(output.ConfigError, "default account did not set", nil)
}
fmt.Println(ReadConfig.DefaultAccount.String())
return nil
case "explorer":
output.PrintResult(ReadConfig.Explorer)
return nil
case "language":
output.PrintResult(ReadConfig.Language)
return nil
case "nsv2height":
fmt.Println(ReadConfig.Nsv2height)
return nil
case "analyserEndpoint":
fmt.Println(ReadConfig.AnalyserEndpoint)
case "all":
fmt.Println(ReadConfig.String())
return nil
}
return nil
}

// GetContextAddressOrAlias gets current context
Expand Down Expand Up @@ -230,6 +227,8 @@ func set(args []string) error {
}
ReadConfig.Endpoint = args[1]
ReadConfig.SecureConnect = !Insecure
case "analyserEndpoint":
ReadConfig.AnalyserEndpoint = args[1]
case "wallet":
ReadConfig.Wallet = args[1]
case "explorer":
Expand Down Expand Up @@ -293,6 +292,7 @@ func reset() error {
ReadConfig.DefaultAccount = *new(Context)
ReadConfig.Explorer = "iotexscan"
ReadConfig.Language = "English"
ReadConfig.AnalyserEndpoint = defaultAnalyserEndpoint

err := writeConfig()
if err != nil {
Expand Down

0 comments on commit 44e0a68

Please sign in to comment.