From 8a94adbbfec341a8148f4682bafcab6fa3c793df Mon Sep 17 00:00:00 2001 From: saito Date: Wed, 29 Nov 2023 07:58:46 +0800 Subject: [PATCH] feat(ioctl): support znode project management commands(based #3987) (#3989) * feat(ioctl): add znode commands configs * feat(ioctl): add znode commands configs * feat(ioctl): support znode message committing and querying commands(based #3986) * chore: code fmt * draft * feat(ioctl): support ws project mgr commands * feat(ioctl): support w3bstream message committing and querying commands(based #3986) * feat(ioctl): ws node project management * feat(ioctl): ws node project management * feat(ioctl): ws node project management * feat(ioctl): ws node project management * feat(ioctl): ws node project management * feat(ioctl): ws node project management * feat(ioctl): ws node project management --------- Co-authored-by: CoderZhi --- ioctl/cmd/action/action.go | 96 +-- ioctl/cmd/action/actionhash.go | 7 +- ioctl/cmd/action/actionsendraw.go | 3 +- ioctl/cmd/action/xrc20.go | 3 +- ioctl/cmd/action/xrc20const.go | 3 +- ioctl/cmd/contract/contract_test.go | 2 +- ioctl/cmd/contract/contracttestbytecode.go | 3 +- ioctl/cmd/contract/contracttestfunction.go | 5 +- ioctl/cmd/contract/parse.go | 3 +- ioctl/cmd/ws/ws.go | 1 + ioctl/cmd/ws/wsproject.go | 180 ++++++ ioctl/cmd/ws/wsproject.json | 690 +++++++++++++++++++++ ioctl/cmd/ws/wsprojectcreate.go | 98 +++ ioctl/cmd/ws/wsprojectquery.go | 66 ++ ioctl/cmd/ws/wsprojectupdate.go | 75 +++ 15 files changed, 1182 insertions(+), 53 deletions(-) create mode 100644 ioctl/cmd/ws/wsproject.go create mode 100644 ioctl/cmd/ws/wsproject.json create mode 100644 ioctl/cmd/ws/wsprojectcreate.go create mode 100644 ioctl/cmd/ws/wsprojectquery.go create mode 100644 ioctl/cmd/ws/wsprojectupdate.go diff --git a/ioctl/cmd/action/action.go b/ioctl/cmd/action/action.go index bb502b0d5f..dbcb528552 100644 --- a/ioctl/cmd/action/action.go +++ b/ioctl/cmd/action/action.go @@ -13,14 +13,13 @@ import ( "strings" "github.com/grpc-ecosystem/go-grpc-middleware/util/metautils" - "github.com/spf13/cobra" - "google.golang.org/grpc/status" - "google.golang.org/protobuf/proto" - "github.com/iotexproject/go-pkgs/hash" "github.com/iotexproject/iotex-address/address" "github.com/iotexproject/iotex-proto/golang/iotexapi" "github.com/iotexproject/iotex-proto/golang/iotextypes" + "github.com/spf13/cobra" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" "github.com/iotexproject/iotex-core/action" "github.com/iotexproject/iotex-core/ioctl/cmd/account" @@ -212,26 +211,11 @@ func fixGasLimit(caller string, execution *action.Execution) (*action.Execution, // SendRaw sends raw action to blockchain func SendRaw(selp *iotextypes.Action) error { - conn, err := util.ConnectToEndpoint(config.ReadConfig.SecureConnect && !config.Insecure) + _, err := SendRawAndRespond(selp) 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) + return err } - request := &iotexapi.SendActionRequest{Action: selp} - if _, err = cli.SendAction(ctx, request); err != nil { - if sta, ok := status.FromError(err); ok { - return output.NewError(output.APIError, sta.Message(), nil) - } - return output.NewError(output.NetworkError, "failed to invoke SendAction api", err) - } shash := hash.Hash256b(byteutil.Must(proto.Marshal(selp))) txhash := hex.EncodeToString(shash[:]) message := sendMessage{Info: "Action has been sent to blockchain.", TxHash: txhash, URL: "https://"} @@ -250,16 +234,48 @@ func SendRaw(selp *iotextypes.Action) error { return nil } +// SendRawAndRespond sends raw action to blockchain with response and error return +func SendRawAndRespond(selp *iotextypes.Action) (*iotexapi.SendActionResponse, error) { + conn, err := util.ConnectToEndpoint(config.ReadConfig.SecureConnect && !config.Insecure) + if err != nil { + return nil, 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) + } + + request := &iotexapi.SendActionRequest{Action: selp} + response, err := cli.SendAction(ctx, request) + if err != nil { + if sta, ok := status.FromError(err); ok { + return nil, output.NewError(output.APIError, sta.Message(), nil) + } + return nil, output.NewError(output.NetworkError, "failed to invoke SendAction api", err) + } + return response, nil +} + // SendAction sends signed action to blockchain func SendAction(elp action.Envelope, signer string) error { + _, err := SendActionAndResponse(elp, signer) + return err +} + +// SendActionAndResponse sends signed action to blockchain with response and error return +func SendActionAndResponse(elp action.Envelope, signer string) (*iotexapi.SendActionResponse, error) { prvKey, err := account.PrivateKeyFromSigner(signer, _passwordFlag.Value().(string)) if err != nil { - return err + return nil, err } chainMeta, err := bc.GetChainMeta() if err != nil { - return output.NewError(0, "failed to get chain meta", err) + return nil, output.NewError(0, "failed to get chain meta", err) } elp.SetChainID(chainMeta.GetChainID()) @@ -268,7 +284,7 @@ func SendAction(elp action.Envelope, signer string) error { signer = addr.String() nonce, err := nonce(signer) if err != nil { - return output.NewError(0, "failed to get nonce ", err) + return nil, output.NewError(0, "failed to get nonce ", err) } elp.SetNonce(nonce) } @@ -276,16 +292,16 @@ func SendAction(elp action.Envelope, signer string) error { sealed, err := action.Sign(elp, prvKey) prvKey.Zero() if err != nil { - return output.NewError(output.CryptoError, "failed to sign action", err) + return nil, output.NewError(output.CryptoError, "failed to sign action", err) } if err := isBalanceEnough(signer, sealed); err != nil { - return output.NewError(0, "failed to pass balance check", err) // TODO: undefined error + return nil, output.NewError(0, "failed to pass balance check", err) // TODO: undefined error } selp := sealed.Proto() actionInfo, err := printActionProto(selp) if err != nil { - return output.NewError(0, "failed to print action proto message", err) + return nil, output.NewError(0, "failed to print action proto message", err) } if _yesFlag.Value() == false { @@ -295,47 +311,53 @@ func SendAction(elp action.Envelope, signer string) error { fmt.Println(message.String()) if _, err := fmt.Scanf("%s", &confirm); err != nil { - return output.NewError(output.InputError, "failed to input yes", err) + return nil, output.NewError(output.InputError, "failed to input yes", err) } if !strings.EqualFold(confirm, "yes") { output.PrintResult("quit") - return nil + return nil, nil } } - return SendRaw(selp) + return SendRawAndRespond(selp) } // Execute sends signed execution transaction to blockchain func Execute(contract string, amount *big.Int, bytecode []byte) error { + _, err := ExecuteAndResponse(contract, amount, bytecode) + return err +} + +// ExecuteAndResponse sends signed execution transaction to blockchain and with response and error return +func ExecuteAndResponse(contract string, amount *big.Int, bytecode []byte) (*iotexapi.SendActionResponse, error) { if len(contract) == 0 && len(bytecode) == 0 { - return output.NewError(output.InputError, "failed to deploy contract with empty bytecode", nil) + return nil, output.NewError(output.InputError, "failed to deploy contract with empty bytecode", nil) } gasPriceRau, err := gasPriceInRau() if err != nil { - return output.NewError(0, "failed to get gas price", err) + return nil, output.NewError(0, "failed to get gas price", err) } signer, err := Signer() if err != nil { - return output.NewError(output.AddressError, "failed to get signer address", err) + return nil, output.NewError(output.AddressError, "failed to get signer address", err) } nonce, err := nonce(signer) if err != nil { - return output.NewError(0, "failed to get nonce", err) + return nil, output.NewError(0, "failed to get nonce", err) } gasLimit := _gasLimitFlag.Value().(uint64) tx, err := action.NewExecution(contract, nonce, amount, gasLimit, gasPriceRau, bytecode) if err != nil || tx == nil { - return output.NewError(output.InstantiationError, "failed to make a Execution instance", err) + return nil, output.NewError(output.InstantiationError, "failed to make a Execution instance", err) } if gasLimit == 0 { tx, err = fixGasLimit(signer, tx) if err != nil || tx == nil { - return output.NewError(0, "failed to fix Execution gaslimit", err) + return nil, output.NewError(0, "failed to fix Execution gaslimit", err) } gasLimit = tx.GasLimit() } - return SendAction( + return SendActionAndResponse( (&action.EnvelopeBuilder{}). SetNonce(nonce). SetGasPrice(gasPriceRau). diff --git a/ioctl/cmd/action/actionhash.go b/ioctl/cmd/action/actionhash.go index 30b0b53f04..cec795b92a 100644 --- a/ioctl/cmd/action/actionhash.go +++ b/ioctl/cmd/action/actionhash.go @@ -15,19 +15,18 @@ import ( protoV1 "github.com/golang/protobuf/proto" "github.com/grpc-ecosystem/go-grpc-middleware/util/metautils" + "github.com/iotexproject/go-pkgs/crypto" + "github.com/iotexproject/iotex-proto/golang/iotexapi" + "github.com/iotexproject/iotex-proto/golang/iotextypes" "github.com/spf13/cobra" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "github.com/iotexproject/go-pkgs/crypto" - "github.com/iotexproject/iotex-proto/golang/iotextypes" - "github.com/iotexproject/iotex-core/action/protocol/staking" "github.com/iotexproject/iotex-core/ioctl/cmd/alias" "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" ) // Multi-language support diff --git a/ioctl/cmd/action/actionsendraw.go b/ioctl/cmd/action/actionsendraw.go index 19227b10ac..77ca89bbda 100644 --- a/ioctl/cmd/action/actionsendraw.go +++ b/ioctl/cmd/action/actionsendraw.go @@ -8,11 +8,10 @@ package action import ( "encoding/hex" + "github.com/iotexproject/iotex-proto/golang/iotextypes" "github.com/spf13/cobra" "google.golang.org/protobuf/proto" - "github.com/iotexproject/iotex-proto/golang/iotextypes" - "github.com/iotexproject/iotex-core/ioctl/config" "github.com/iotexproject/iotex-core/ioctl/output" ) diff --git a/ioctl/cmd/action/xrc20.go b/ioctl/cmd/action/xrc20.go index 9f72d21958..8558f7b4af 100644 --- a/ioctl/cmd/action/xrc20.go +++ b/ioctl/cmd/action/xrc20.go @@ -11,9 +11,8 @@ import ( "math/big" "strconv" - "github.com/spf13/cobra" - "github.com/iotexproject/iotex-address/address" + "github.com/spf13/cobra" "github.com/iotexproject/iotex-core/ioctl/cmd/alias" "github.com/iotexproject/iotex-core/ioctl/config" diff --git a/ioctl/cmd/action/xrc20const.go b/ioctl/cmd/action/xrc20const.go index 60fc1bf23e..a53f5d58f1 100644 --- a/ioctl/cmd/action/xrc20const.go +++ b/ioctl/cmd/action/xrc20const.go @@ -4,8 +4,9 @@ import ( "strings" "github.com/ethereum/go-ethereum/accounts/abi" - "github.com/iotexproject/iotex-core/pkg/log" "go.uber.org/zap" + + "github.com/iotexproject/iotex-core/pkg/log" ) const _abiConst = `[ diff --git a/ioctl/cmd/contract/contract_test.go b/ioctl/cmd/contract/contract_test.go index f12006202b..821ebc14b5 100644 --- a/ioctl/cmd/contract/contract_test.go +++ b/ioctl/cmd/contract/contract_test.go @@ -108,7 +108,7 @@ func TestParseOutput(t *testing.T) { } for _, test := range tests { - v, err := parseOutput(testAbi, test.method, test.outputs) + v, err := ParseOutput(testAbi, test.method, test.outputs) r.NoError(err) r.Equal(test.expectResult, fmt.Sprint(v)) } diff --git a/ioctl/cmd/contract/contracttestbytecode.go b/ioctl/cmd/contract/contracttestbytecode.go index 24a1d06a5b..e482885d78 100644 --- a/ioctl/cmd/contract/contracttestbytecode.go +++ b/ioctl/cmd/contract/contracttestbytecode.go @@ -8,9 +8,8 @@ package contract import ( "math/big" - "github.com/spf13/cobra" - "github.com/iotexproject/iotex-address/address" + "github.com/spf13/cobra" "github.com/iotexproject/iotex-core/ioctl/cmd/action" "github.com/iotexproject/iotex-core/ioctl/config" diff --git a/ioctl/cmd/contract/contracttestfunction.go b/ioctl/cmd/contract/contracttestfunction.go index 04979fd041..d1647d6d0d 100644 --- a/ioctl/cmd/contract/contracttestfunction.go +++ b/ioctl/cmd/contract/contracttestfunction.go @@ -8,9 +8,8 @@ package contract import ( "math/big" - "github.com/spf13/cobra" - "github.com/iotexproject/iotex-address/address" + "github.com/spf13/cobra" "github.com/iotexproject/iotex-core/ioctl/cmd/action" "github.com/iotexproject/iotex-core/ioctl/config" @@ -80,7 +79,7 @@ func contractTestFunction(args []string) error { return err } - result, err := parseOutput(abi, methodName, rowResult) + result, err := ParseOutput(abi, methodName, rowResult) if err != nil { result = rowResult } diff --git a/ioctl/cmd/contract/parse.go b/ioctl/cmd/contract/parse.go index 4766d770e5..18ab531f78 100644 --- a/ioctl/cmd/contract/parse.go +++ b/ioctl/cmd/contract/parse.go @@ -44,7 +44,8 @@ func parseInput(rowInput string) (map[string]interface{}, error) { return input, nil } -func parseOutput(targetAbi *abi.ABI, targetMethod string, result string) (string, error) { +// ParseOutput parse contract output data +func ParseOutput(targetAbi *abi.ABI, targetMethod string, result string) (string, error) { resultBytes, err := hex.DecodeString(result) if err != nil { return "", output.NewError(output.ConvertError, "failed to decode result", err) diff --git a/ioctl/cmd/ws/ws.go b/ioctl/cmd/ws/ws.go index 2f7d1b4dec..b4c0fedba0 100644 --- a/ioctl/cmd/ws/ws.go +++ b/ioctl/cmd/ws/ws.go @@ -32,6 +32,7 @@ var ( func init() { WsCmd.AddCommand(wsMessage) + WsCmd.AddCommand(wsProject) WsCmd.PersistentFlags().StringVar( &config.ReadConfig.Endpoint, "endpoint", diff --git a/ioctl/cmd/ws/wsproject.go b/ioctl/cmd/ws/wsproject.go new file mode 100644 index 0000000000..16938343a4 --- /dev/null +++ b/ioctl/cmd/ws/wsproject.go @@ -0,0 +1,180 @@ +package ws + +import ( + "bytes" + "context" + _ "embed" // import ws project ABI + "encoding/hex" + "reflect" + "time" + + "github.com/cenkalti/backoff" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/grpc-ecosystem/go-grpc-middleware/util/metautils" + "github.com/iotexproject/iotex-proto/golang/iotexapi" + "github.com/iotexproject/iotex-proto/golang/iotextypes" + "github.com/pkg/errors" + "github.com/spf13/cobra" + "go.uber.org/zap" + "google.golang.org/grpc/codes" + "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-core/pkg/log" +) + +var ( + // wsProject represents the w3bstream project management command + wsProject = &cobra.Command{ + Use: "project", + Short: config.TranslateInLang(wsProjectShorts, config.UILanguage), + } + + // wsProjectShorts w3bstream project shorts multi-lang support + wsProjectShorts = map[config.Language]string{ + config.English: "w3bstream project management", + config.Chinese: "w3bstream项目管理", + } + + _flagProjectRegisterContractAddressUsages = map[config.Language]string{ + config.English: "project register contract address", + config.Chinese: "项目注册合约地址", + } + + wsProjectRegisterContractAddress string + wsProjectRegisterContractABI abi.ABI + + //go:embed wsproject.json + wsProjectRegisterContractJSONABI []byte +) + +const ( + createWsProjectFuncName = "createProject" + createWsProjectEventName = "ProjectUpserted" + updateWsProjectFuncName = "updateProject" + queryWsProjectFuncName = "projects" +) + +func init() { + var err error + wsProjectRegisterContractABI, err = abi.JSON(bytes.NewReader(wsProjectRegisterContractJSONABI)) + if err != nil { + log.L().Panic("cannot get abi JSON data", zap.Error(err)) + } + + wsProject.AddCommand(wsProjectCreate) + wsProject.AddCommand(wsProjectUpdate) + wsProject.AddCommand(wsProjectQuery) + + wsProject.PersistentFlags().StringVarP( + &wsProjectRegisterContractAddress, + "contract-address", + "c", + "", + config.TranslateInLang(_flagProjectRegisterContractAddressUsages, config.UILanguage), + ) + _ = wsProject.MarkFlagRequired("contract-address") +} + +func convertStringToAbiBytes32(hash string) (interface{}, error) { + t, _ := abi.NewType("bytes32", "", nil) + + bytecode, err := hex.DecodeString(util.TrimHexPrefix(hash)) + if err != nil { + return nil, err + } + + if t.Size != len(bytecode) { + return nil, errors.New("invalid arg") + } + + bytesType := reflect.ArrayOf(t.Size, reflect.TypeOf(uint8(0))) + bytesVal := reflect.New(bytesType).Elem() + + for i, b := range bytecode { + bytesVal.Index(i).Set(reflect.ValueOf(b)) + } + + return bytesVal.Interface(), nil +} + +func waitReceiptByActionHash(h string) (*iotexapi.GetReceiptByActionResponse, error) { + conn, err := util.ConnectToEndpoint(config.ReadConfig.SecureConnect && !config.Insecure) + if err != nil { + return nil, 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) + } + + var rsp *iotexapi.GetReceiptByActionResponse + err = backoff.Retry(func() error { + rsp, err = cli.GetReceiptByAction(ctx, &iotexapi.GetReceiptByActionRequest{ + ActionHash: h, + }) + return err + }, backoff.WithMaxRetries(backoff.NewConstantBackOff(30*time.Second), 3)) + if err != nil { + sta, ok := status.FromError(err) + if ok && sta.Code() == codes.NotFound { + return nil, output.NewError(output.APIError, "not found", nil) + } else if ok { + return nil, output.NewError(output.APIError, sta.Message(), nil) + } + return nil, output.NewError(output.NetworkError, "failed to invoke GetReceiptByAction api", err) + } + return rsp, nil +} + +func getEventInputsByName(logs []*iotextypes.Log, eventName string) (map[string]any, error) { + var ( + abievent *abi.Event + log *iotextypes.Log + ) + for _, l := range logs { + evabi, err := wsProjectRegisterContractABI.EventByID(common.BytesToHash(l.Topics[0])) + if err != nil { + return nil, errors.Wrapf(err, "get event abi from topic %v failed", l.Topics[0]) + } + if evabi.Name == eventName { + abievent, log = evabi, l + break + } + } + + if abievent == nil || log == nil { + return nil, errors.Errorf("event not found: %s", eventName) + } + + inputs := make(map[string]any) + if len(log.Data) > 0 { + if err := abievent.Inputs.UnpackIntoMap(inputs, log.Data); err != nil { + return nil, errors.Wrap(err, "unpack event data failed") + } + } + args := make(abi.Arguments, 0) + for _, arg := range abievent.Inputs { + if arg.Indexed { + args = append(args, arg) + } + } + topics := make([]common.Hash, 0) + for i, topic := range log.Topics { + if i > 0 { + topics = append(topics, common.BytesToHash(topic)) + } + } + if err := abi.ParseTopicsIntoMap(inputs, args, topics); err != nil { + return nil, errors.Wrap(err, "unpack event indexed fields failed") + } + + return inputs, nil +} diff --git a/ioctl/cmd/ws/wsproject.json b/ioctl/cmd/ws/wsproject.json new file mode 100644 index 0000000000..0cdd79290d --- /dev/null +++ b/ioctl/cmd/ws/wsproject.json @@ -0,0 +1,690 @@ +[ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "ERC721IncorrectOwner", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "ERC721InsufficientApproval", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "approver", + "type": "address" + } + ], + "name": "ERC721InvalidApprover", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "name": "ERC721InvalidOperator", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "ERC721InvalidOwner", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "receiver", + "type": "address" + } + ], + "name": "ERC721InvalidReceiver", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "ERC721InvalidSender", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "ERC721NonexistentToken", + "type": "error" + }, + { + "inputs": [], + "name": "ReentrancyGuardReentrantCall", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "approved", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "ApprovalForAll", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint64", + "name": "projectId", + "type": "uint64" + }, + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "name": "OperatorAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint64", + "name": "projectId", + "type": "uint64" + }, + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "name": "OperatorRemoved", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint64", + "name": "projectId", + "type": "uint64" + } + ], + "name": "ProjectPaused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint64", + "name": "projectId", + "type": "uint64" + } + ], + "name": "ProjectUnpaused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint64", + "name": "projectId", + "type": "uint64" + }, + { + "indexed": false, + "internalType": "string", + "name": "uri", + "type": "string" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "hash", + "type": "bytes32" + } + ], + "name": "ProjectUpserted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "_projectId", + "type": "uint64" + }, + { + "internalType": "address", + "name": "_operator", + "type": "address" + } + ], + "name": "addOperator", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_operator", + "type": "address" + }, + { + "internalType": "uint64", + "name": "_projectId", + "type": "uint64" + } + ], + "name": "canOperateProject", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "_uri", + "type": "string" + }, + { + "internalType": "bytes32", + "name": "_hash", + "type": "bytes32" + } + ], + "name": "createProject", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "getApproved", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "name": "isApprovedForAll", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "ownerOf", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "_projectId", + "type": "uint64" + } + ], + "name": "pauseProject", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "name": "projects", + "outputs": [ + { + "internalType": "string", + "name": "uri", + "type": "string" + }, + { + "internalType": "bytes32", + "name": "hash", + "type": "bytes32" + }, + { + "internalType": "bool", + "name": "paused", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "_projectId", + "type": "uint64" + }, + { + "internalType": "address", + "name": "_operator", + "type": "address" + } + ], + "name": "removeOperator", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "safeTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "safeTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "setApprovalForAll", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "interfaceId", + "type": "bytes4" + } + ], + "name": "supportsInterface", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "tokenURI", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "_projectId", + "type": "uint64" + } + ], + "name": "unpauseProject", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "_projectId", + "type": "uint64" + }, + { + "internalType": "string", + "name": "_uri", + "type": "string" + }, + { + "internalType": "bytes32", + "name": "_hash", + "type": "bytes32" + } + ], + "name": "updateProject", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] \ No newline at end of file diff --git a/ioctl/cmd/ws/wsprojectcreate.go b/ioctl/cmd/ws/wsprojectcreate.go new file mode 100644 index 0000000000..ed4131dbb6 --- /dev/null +++ b/ioctl/cmd/ws/wsprojectcreate.go @@ -0,0 +1,98 @@ +package ws + +import ( + "fmt" + "math/big" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + + "github.com/iotexproject/iotex-core/ioctl/cmd/action" + "github.com/iotexproject/iotex-core/ioctl/config" + "github.com/iotexproject/iotex-core/ioctl/output" + "github.com/iotexproject/iotex-core/ioctl/util" +) + +var ( + // wsProjectCreate represents the create w3bstream project command + wsProjectCreate = &cobra.Command{ + Use: "create", + Short: config.TranslateInLang(wsProjectCreateShorts, config.UILanguage), + RunE: func(cmd *cobra.Command, args []string) error { + uri, err := cmd.Flags().GetString("project-uri") + if err != nil { + return output.PrintError(err) + } + hash, err := cmd.Flags().GetString("project-hash") + if err != nil { + return output.PrintError(err) + } + out, err := createProject(uri, hash) + if err != nil { + return output.PrintError(err) + } + output.PrintResult(out) + return nil + }, + } + + // wsProjectCreateShorts create w3bstream project shorts multi-lang support + wsProjectCreateShorts = map[config.Language]string{ + config.English: "create w3bstream project", + config.Chinese: "创建项目", + } + + _flagProjectURIUsages = map[config.Language]string{ + config.English: "project config fetch uri", + config.Chinese: "项目配置拉取地址", + } + _flagProjectHashUsages = map[config.Language]string{ + config.English: "project config hash for validating", + config.Chinese: "项目配置hash", + } +) + +func init() { + wsProjectCreate.Flags().StringP("project-uri", "u", "", config.TranslateInLang(_flagProjectURIUsages, config.UILanguage)) + wsProjectCreate.Flags().StringP("project-hash", "v", "", config.TranslateInLang(_flagProjectHashUsages, config.UILanguage)) + + _ = wsProjectCreate.MarkFlagRequired("project-uri") + _ = wsProjectCreate.MarkFlagRequired("project-hash") +} + +func createProject(uri, hash string) (string, error) { + contract, err := util.Address(wsProjectRegisterContractAddress) + if err != nil { + return "", output.NewError(output.AddressError, "failed to get project register contract address", err) + } + + hashArg, err := convertStringToAbiBytes32(hash) + if err != nil { + return "", err + } + + bytecode, err := wsProjectRegisterContractABI.Pack(createWsProjectFuncName, uri, hashArg) + if err != nil { + return "", output.NewError(output.ConvertError, fmt.Sprintf("failed to pack abi"), err) + } + + res, err := action.ExecuteAndResponse(contract, big.NewInt(0), bytecode) + if err != nil { + return "", errors.Wrap(err, "execute contract failed") + } + + r, err := waitReceiptByActionHash(res.ActionHash) + if err != nil { + return "", errors.Wrap(err, "wait contract execution receipt failed") + } + + inputs, err := getEventInputsByName(r.ReceiptInfo.Receipt.Logs, createWsProjectEventName) + if err != nil { + return "", errors.Wrap(err, "get receipt event failed") + } + projectid, ok := inputs["projectId"] + if !ok { + return "", errors.New("result not found in event inputs") + } + return fmt.Sprintf("Your project is successfully created. project id is : %d", projectid), nil +} diff --git a/ioctl/cmd/ws/wsprojectquery.go b/ioctl/cmd/ws/wsprojectquery.go new file mode 100644 index 0000000000..57d76162a7 --- /dev/null +++ b/ioctl/cmd/ws/wsprojectquery.go @@ -0,0 +1,66 @@ +package ws + +import ( + "fmt" + + "github.com/iotexproject/iotex-address/address" + "github.com/pkg/errors" + "github.com/spf13/cobra" + + "github.com/iotexproject/iotex-core/ioctl/cmd/action" + "github.com/iotexproject/iotex-core/ioctl/cmd/contract" + "github.com/iotexproject/iotex-core/ioctl/config" + "github.com/iotexproject/iotex-core/ioctl/output" + "github.com/iotexproject/iotex-core/ioctl/util" +) + +var ( + // wsProjectQuery represents the query w3bstream project command + wsProjectQuery = &cobra.Command{ + Use: "query", + Short: config.TranslateInLang(wsProjectQueryShorts, config.UILanguage), + RunE: func(cmd *cobra.Command, args []string) error { + id, err := cmd.Flags().GetUint64("project-id") + if err != nil { + return output.PrintError(err) + } + out, err := queryProject(id) + if err != nil { + return output.PrintError(err) + } + output.PrintResult(out) + return nil + }, + } + + // wsProjectQueryShorts query w3bstream project shorts multi-lang support + wsProjectQueryShorts = map[config.Language]string{ + config.English: "query w3bstream project", + config.Chinese: "查询项目", + } +) + +func init() { + wsProjectQuery.Flags().Uint64P("project-id", "i", 0, config.TranslateInLang(_flagProjectIDUsages, config.UILanguage)) + + _ = wsProjectQuery.MarkFlagRequired("project-id") +} + +func queryProject(id uint64) (string, error) { + contractAddr, err := util.Address(wsProjectRegisterContractAddress) + if err != nil { + return "", output.NewError(output.AddressError, "failed to get project register contract address", err) + } + + bytecode, err := wsProjectRegisterContractABI.Pack(queryWsProjectFuncName, id) + if err != nil { + return "", output.NewError(output.ConvertError, fmt.Sprintf("failed to pack abi"), err) + } + + addr, _ := address.FromString(contractAddr) + data, err := action.Read(addr, "0", bytecode) + if err != nil { + return "", errors.Wrap(err, "read contract failed") + } + return contract.ParseOutput(&wsProjectRegisterContractABI, queryWsProjectFuncName, data) +} diff --git a/ioctl/cmd/ws/wsprojectupdate.go b/ioctl/cmd/ws/wsprojectupdate.go new file mode 100644 index 0000000000..13a887ea71 --- /dev/null +++ b/ioctl/cmd/ws/wsprojectupdate.go @@ -0,0 +1,75 @@ +package ws + +import ( + "fmt" + "math/big" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + + "github.com/iotexproject/iotex-core/ioctl/cmd/action" + "github.com/iotexproject/iotex-core/ioctl/config" + "github.com/iotexproject/iotex-core/ioctl/output" + "github.com/iotexproject/iotex-core/ioctl/util" +) + +var ( + // wsProjectUpdate represents the update w3bstream project command + wsProjectUpdate = &cobra.Command{ + Use: "update", + Short: config.TranslateInLang(wsProjectUpdateShorts, config.UILanguage), + RunE: func(cmd *cobra.Command, args []string) error { + id, err := cmd.Flags().GetUint64("project-id") + if err != nil { + return output.PrintError(err) + } + uri, err := cmd.Flags().GetString("project-uri") + if err != nil { + return output.PrintError(err) + } + hash, err := cmd.Flags().GetString("project-hash") + if err != nil { + return output.PrintError(err) + } + return output.PrintError(updateProject(id, uri, hash)) + }, + } + + // wsProjectUpdateShorts update w3bstream project shorts multi-lang support + wsProjectUpdateShorts = map[config.Language]string{ + config.English: "update w3bstream project", + config.Chinese: "更新项目", + } +) + +func init() { + wsProjectUpdate.Flags().Uint64P("project-id", "i", 0, config.TranslateInLang(_flagProjectIDUsages, config.UILanguage)) + wsProjectUpdate.Flags().StringP("project-uri", "u", "", config.TranslateInLang(_flagProjectURIUsages, config.UILanguage)) + wsProjectUpdate.Flags().StringP("project-hash", "v", "", config.TranslateInLang(_flagProjectHashUsages, config.UILanguage)) + + _ = wsProjectCreate.MarkFlagRequired("project-id") + _ = wsProjectCreate.MarkFlagRequired("project-uri") + _ = wsProjectCreate.MarkFlagRequired("project-hash") +} + +func updateProject(projectID uint64, uri, hash string) error { + contract, err := util.Address(wsProjectRegisterContractAddress) + if err != nil { + return output.NewError(output.AddressError, "failed to get project register contract address", err) + } + + hashArg, err := convertStringToAbiBytes32(hash) + if err != nil { + return errors.Wrap(err, "convert input arg failed") + } + + bytecode, err := wsProjectRegisterContractABI.Pack( + updateWsProjectFuncName, + projectID, uri, hashArg, + ) + if err != nil { + return output.NewError(output.ConvertError, fmt.Sprintf("failed to pack abi"), err) + } + + return action.Execute(contract, big.NewInt(0), bytecode) +}