diff --git a/ioctl/cmd/ws/ws.go b/ioctl/cmd/ws/ws.go index 95f13f2b6a..4e4e097ddb 100644 --- a/ioctl/cmd/ws/ws.go +++ b/ioctl/cmd/ws/ws.go @@ -4,6 +4,7 @@ import ( "github.com/spf13/cobra" "github.com/iotexproject/iotex-core/ioctl/config" + "github.com/iotexproject/iotex-core/ioctl/flag" ) var ( @@ -13,12 +14,14 @@ var ( Short: config.TranslateInLang(wsCmdShorts, config.UILanguage), } - // wsCmdShorts command multi-lang supports wsCmdShorts = map[config.Language]string{ config.English: "W3bstream node operations", config.Chinese: "W3bstream节点操作", } +) +// flags multi-language +var ( _flagChainEndpointUsages = map[config.Language]string{ config.English: "set endpoint for once", config.Chinese: "一次设置端点", @@ -39,16 +42,46 @@ var ( config.Chinese: "一次设置ipfs网关", } - _flagContractAddressUsages = map[config.Language]string{ + _flagProjectRegisterContractAddressUsages = map[config.Language]string{ config.English: "set w3bsteram project register contract address for once", config.Chinese: "一次设置w3bstream项目注册合约地址", } + + _flagProjectStoreContractAddressUsages = map[config.Language]string{ + config.English: "set w3bsteram project store contract address for once", + config.Chinese: "一次设置w3bstream项目存储合约地址", + } + + _flagFleetManagementContractAddressUsages = map[config.Language]string{ + config.English: "set w3bsteram fleet management contract address for once", + config.Chinese: "一次设置w3bstream项目管理合约地址", + } + + _flagProverStoreContractAddressUsages = map[config.Language]string{ + config.English: "set w3bsteram prover store contract address for once", + config.Chinese: "一次设置w3bstream prover存储合约地址", + } + _flagTransferAmountUsages = map[config.Language]string{ + config.English: "amount(rau) need to pay, default 0", + config.Chinese: "需要支付的token数量(单位rau), 默认0", + } + + _flagProjectDevicesContractAddressUsages = map[config.Language]string{ + config.English: "set w3bsteram project devices contract address for once", + config.Chinese: "一次设置w3bstream project设备合约地址", + } + _flagProjectIDUsages = map[config.Language]string{ + config.English: "project id", + config.Chinese: "项目ID", + } ) -func init() { - WsCmd.AddCommand(wsMessage) - WsCmd.AddCommand(wsProject) +var ( + // transferAmount + transferAmount = flag.NewUint64VarP("amount", "", 0, config.TranslateInLang(_flagTransferAmountUsages, config.UILanguage)) +) +func init() { WsCmd.PersistentFlags().StringVar( &config.ReadConfig.Endpoint, "endpoint", config.ReadConfig.Endpoint, config.TranslateInLang(_flagChainEndpointUsages, config.UILanguage), @@ -66,7 +99,23 @@ func init() { config.ReadConfig.IPFSGateway, config.TranslateInLang(_flagIPFSGatewayUsages, config.UILanguage), ) WsCmd.PersistentFlags().StringVar( - &config.ReadConfig.WsProjectRegisterContract, "contract-address", - config.ReadConfig.WsProjectDevicesContract, config.TranslateInLang(_flagContractAddressUsages, config.UILanguage), + &config.ReadConfig.WsProjectRegisterContract, "project-register-contract", + config.ReadConfig.WsProjectRegisterContract, config.TranslateInLang(_flagProjectRegisterContractAddressUsages, config.UILanguage), + ) + WsCmd.PersistentFlags().StringVar( + &config.ReadConfig.WsProjectStoreContract, "project-store-contract", + config.ReadConfig.WsProjectStoreContract, config.TranslateInLang(_flagProjectStoreContractAddressUsages, config.UILanguage), + ) + WsCmd.PersistentFlags().StringVar( + &config.ReadConfig.WsFleetManagementContract, "fleet-management-contract", + config.ReadConfig.WsFleetManagementContract, config.TranslateInLang(_flagFleetManagementContractAddressUsages, config.UILanguage), + ) + WsCmd.PersistentFlags().StringVar( + &config.ReadConfig.WsProverStoreContract, "prover-store-contract", + config.ReadConfig.WsProverStoreContract, config.TranslateInLang(_flagProverStoreContractAddressUsages, config.UILanguage), + ) + WsCmd.PersistentFlags().StringVar( + &config.ReadConfig.WsProjectDevicesContract, "project-devices-contract", + config.ReadConfig.WsProjectDevicesContract, config.TranslateInLang(_flagProjectDevicesContractAddressUsages, config.UILanguage), ) } diff --git a/ioctl/cmd/ws/ws_test.go b/ioctl/cmd/ws/ws_test.go deleted file mode 100644 index a57028c598..0000000000 --- a/ioctl/cmd/ws/ws_test.go +++ /dev/null @@ -1,56 +0,0 @@ -package ws - -import ( - "testing" - - "github.com/iotexproject/iotex-proto/golang/iotextypes" - "github.com/stretchr/testify/require" -) - -func Test_getEventInputsByName(t *testing.T) { - var ( - eventName = "ProjectUpserted" - eventTopic = []byte{136, 157, 97, 20, 123, 253, 22, 248, 153, 111, 242, 89, 226, 42, 196, 66, 136, 167, 214, 250, 9, 87, 132, 77, 185, 136, 240, 113, 183, 136, 60, 35} - ) - - r := require.New(t) - - inputs := wsProjectRegisterContractABI.Events[eventName].Inputs.NonIndexed() - data, err := inputs.Pack("http://a.b.c", [32]byte{}) - r.NoError(err) - - _, err = inputs.Unpack(data) - r.NoError(err) - - t.Run("FailedToGetEventABI_InvalidTopic", func(t *testing.T) { - _, err = getEventInputsByName([]*iotextypes.Log{{ - Topics: [][]byte{{}}, - }}, "any") - r.Error(err) - }) - t.Run("NotParsedTargetEvent_EmptyLogs", func(t *testing.T) { - _, err := getEventInputsByName([]*iotextypes.Log{}, "any") - r.Error(err) - }) - t.Run("FailedToUnpackIntoMap_InvalidLogData", func(t *testing.T) { - _, err = getEventInputsByName([]*iotextypes.Log{{ - Topics: [][]byte{eventTopic}, - Data: make([]byte, 10), - }}, eventName) - r.Error(err) - }) - t.Run("FailedToParseTopicsIntoMap", func(t *testing.T) { - _, err = getEventInputsByName([]*iotextypes.Log{{ - Topics: [][]byte{eventTopic}, - Data: data, - }}, eventName) - r.Error(err) - }) - t.Run("Succeed", func(t *testing.T) { - _, err = getEventInputsByName([]*iotextypes.Log{{ - Topics: [][]byte{eventTopic, append(make([]byte, 31), byte(11))}, - Data: data, - }}, eventName) - r.NoError(err) - }) -} diff --git a/ioctl/cmd/ws/wsmessage.go b/ioctl/cmd/ws/wsmessage.go index c073c7d2f3..c1ddd2d5a9 100644 --- a/ioctl/cmd/ws/wsmessage.go +++ b/ioctl/cmd/ws/wsmessage.go @@ -30,6 +30,8 @@ var ( func init() { wsMessage.AddCommand(wsMessageSend) wsMessage.AddCommand(wsMessageQuery) + + WsCmd.AddCommand(wsMessage) } type sendMessageReq struct { diff --git a/ioctl/cmd/ws/wsmessagesend.go b/ioctl/cmd/ws/wsmessagesend.go index 9f762914a0..dc70b8d28d 100644 --- a/ioctl/cmd/ws/wsmessagesend.go +++ b/ioctl/cmd/ws/wsmessagesend.go @@ -51,10 +51,6 @@ var ( config.Chinese: "向w3bstream发送消息请求zk证明", } - _flagProjectIDUsages = map[config.Language]string{ - config.English: "project id", - config.Chinese: "项目ID", - } _flagProjectVersionUsages = map[config.Language]string{ config.English: "project version", config.Chinese: "项目版本", diff --git a/ioctl/cmd/ws/wsproject.go b/ioctl/cmd/ws/wsproject.go index 3a01b988c2..ac51475bfc 100644 --- a/ioctl/cmd/ws/wsproject.go +++ b/ioctl/cmd/ws/wsproject.go @@ -2,201 +2,132 @@ package ws import ( "bytes" - "context" "crypto/sha256" - _ "embed" // import ws project ABI + _ "embed" // used to embed contract abi "encoding/hex" "fmt" "os" - "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/go-pkgs/hash" - "github.com/iotexproject/iotex-proto/golang/iotexapi" - "github.com/iotexproject/iotex-proto/golang/iotextypes" shell "github.com/ipfs/go-ipfs-api" "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" + "github.com/iotexproject/iotex-core/ioctl/flag" ) -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项目管理", - } - - wsProjectRegisterContractAddress string - wsProjectRegisterContractABI abi.ABI - - //go:embed wsproject.json - wsProjectRegisterContractJSONABI []byte - wsProjectIPFSEndpoint string - wsProjectIPFSGatewayEndpoint string -) +type Command struct { +} -// Errors +// flags var ( - errProjectConfigHashUnmatched = errors.New("project config hash unmatched") - errProjectConfigReadFailed = errors.New("failed to read project config file") - errUploadProjectConfigFailed = errors.New("failed to upload project config file") + projectFilePath = flag.NewStringVarP("path", "", "", config.TranslateInLang(_flagProjectFileUsage, config.UILanguage)) + projectFileHash = flag.NewStringVarP("hash", "", "", config.TranslateInLang(_flagProjectHashUsage, config.UILanguage)) + projectID = flag.NewUint64VarP("id", "", 0, config.TranslateInLang(_flagProjectIDUsage, config.UILanguage)) + projectAttrKey = flag.NewStringVarP("key", "", "", config.TranslateInLang(_flagProjectAttributeKeyUsage, config.UILanguage)) + projectAttrVal = flag.NewStringVarP("val", "", "", config.TranslateInLang(_flagProjectAttributeValUsage, config.UILanguage)) ) -// Constants -const ( - createWsProjectFuncName = "createProject" - updateWsProjectFuncName = "updateProject" - queryWsProjectFuncName = "projects" - wsProjectUpsertedEventName = "ProjectUpserted" -) - -type projectMeta struct { - ProjectID uint64 `json:"projectID"` - URI string `json:"uri"` - HashSha256 string `json:"hashSha256"` -} - -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)) +// flag multi-languages +var ( + _flagProjectFileUsage = map[config.Language]string{ + config.English: "project config file path", + config.Chinese: "项目配置文件路径", } - wsProject.AddCommand(wsProjectCreate) - wsProject.AddCommand(wsProjectUpdate) - wsProject.AddCommand(wsProjectQuery) - wsProject.AddCommand(wsProjectConfig) - - wsProjectRegisterContractAddress = config.ReadConfig.WsProjectRegisterContract - wsProjectIPFSEndpoint = config.ReadConfig.IPFSEndpoint - wsProjectIPFSGatewayEndpoint = config.ReadConfig.IPFSGateway -} - -func convertStringToAbiBytes32(hash string) (interface{}, error) { - t, _ := abi.NewType("bytes32", "", nil) - - bytecode, err := hex.DecodeString(util.TrimHexPrefix(hash)) - if err != nil { - return nil, err + _flagProjectHashUsage = map[config.Language]string{ + config.English: "project config file hash", + config.Chinese: "项目配置文件哈希", } - if t.Size != len(bytecode) { - return nil, errors.New("invalid arg") + _flagProjectIDUsage = map[config.Language]string{ + config.English: "project id", + config.Chinese: "项目ID", } - bytesType := reflect.ArrayOf(t.Size, reflect.TypeOf(uint8(0))) - bytesVal := reflect.New(bytesType).Elem() + _flagProjectAttributeKeyUsage = map[config.Language]string{ + config.English: "project attribute key", + config.Chinese: "项目属性键", + } - for i, b := range bytecode { - bytesVal.Index(i).Set(reflect.ValueOf(b)) + _flagProjectAttributeValUsage = map[config.Language]string{ + config.English: "project attribute value", + config.Chinese: "项目属性值", } +) - return bytesVal.Interface(), nil +var wsProject = &cobra.Command{ + Use: "project", + Short: config.TranslateInLang(map[config.Language]string{ + config.English: "w3bstream project management", + config.Chinese: "w3bstream项目管理", + }, config.UILanguage), } -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) - } +// ioctl global configuration +var ( + ipfsEndpoint = "ipfs.mainnet.iotex.io" +) - 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 -} +// contracts and abis +var ( + //go:embed contracts/abis/ProjectRegistrar.json + projectRegistrarJSON []byte + projectRegistrarABI abi.ABI + projectRegistrarAddr string + + //go:embed contracts/abis/W3bstreamProject.json + projectStoreJSON []byte + projectStoreABI abi.ABI + projectStoreAddress string +) -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 - } - } +// contract function +const ( + funcProjectRegister = "register" + funcUpdateProjectConfig = "updateConfig" + funcQueryProject = "config" + funcPauseProject = "pause" + funcResumeProject = "resume" + funcIsProjectPaused = "isPaused" + funcGetProjectAttr = "attribute" + funcSetProjectAttr = "setAttributes" + funcQueryProjectOwner = "ownerOf" +) - if abievent == nil || log == nil { - return nil, errors.Errorf("event not found: %s", eventName) - } +// contract events care about +const ( + eventOnProjectRegistered = "ProjectBinded" + eventProjectConfigUpdated = "ProjectConfigUpdated" + eventProjectPaused = "ProjectPaused" + eventProjectResumed = "ProjectResumed" + eventProjectAttrSet = "AttributeSet" +) - 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)) - } +func init() { + var err error + projectRegistrarABI, err = abi.JSON(bytes.NewReader(projectRegistrarJSON)) + if err != nil { + panic(err) } - if err := abi.ParseTopicsIntoMap(inputs, args, topics); err != nil { - return nil, errors.Wrap(err, "unpack event indexed fields failed") + projectRegistrarAddr = config.ReadConfig.WsProjectRegisterContract + projectStoreABI, err = abi.JSON(bytes.NewReader(projectStoreJSON)) + if err != nil { + panic(err) } + projectStoreAddress = config.ReadConfig.WsProjectStoreContract - return inputs, nil + WsCmd.AddCommand(wsProject) } -// upload content to endpoint, returns fetch url, content hash and error -func upload(endpoint string, filename, hashstr string) (string, hash.Hash256, error) { +// uploadToIPFS uploads content to IPFS, returns fetch url and content hash +func uploadToIPFS(endpoint string, filename, hashstr string) (string, hash.Hash256, error) { // read file content content, err := os.ReadFile(filename) if err != nil { - err = errors.Wrap(err, errProjectConfigReadFailed.Error()) + err = errors.Wrapf(err, "failed to read file: %s", filename) return "", hash.ZeroHash256, err } @@ -209,7 +140,7 @@ func upload(endpoint string, filename, hashstr string) (string, hash.Hash256, er return "", hash.ZeroHash256, err } if hashInput != hash256b { - return "", hash.ZeroHash256, errProjectConfigHashUnmatched + return "", hash.ZeroHash256, errors.Errorf("failed to validate file hash, expect %s actually %s", hashstr, hex.EncodeToString(hash256b[:])) } } @@ -220,13 +151,13 @@ func upload(endpoint string, filename, hashstr string) (string, hash.Hash256, er ) cid, err = sh.Add(bytes.NewReader(content)) if err != nil { - return "", hash.ZeroHash256, errors.Wrap(err, errUploadProjectConfigFailed.Error()) + return "", hash.ZeroHash256, errors.Wrap(err, "failed to upload file to IPFS") } err = sh.Pin(cid) if err != nil { - return "", hash.ZeroHash256, errors.Wrap(err, errUploadProjectConfigFailed.Error()) + return "", hash.ZeroHash256, errors.Wrapf(err, "failed to pin file to IPFS, cid: %s", cid) } - return fmt.Sprintf("ipfs://%s/%s", wsProjectIPFSEndpoint, cid), hash256b, nil + return fmt.Sprintf("ipfs://%s/%s", endpoint, cid), hash256b, nil } diff --git a/ioctl/cmd/ws/wsproject.json b/ioctl/cmd/ws/wsproject.json deleted file mode 100644 index 0cdd79290d..0000000000 --- a/ioctl/cmd/ws/wsproject.json +++ /dev/null @@ -1,690 +0,0 @@ -[ - { - "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/wsprojectattribute.go b/ioctl/cmd/ws/wsprojectattribute.go new file mode 100644 index 0000000000..b6fa3e4c5e --- /dev/null +++ b/ioctl/cmd/ws/wsprojectattribute.go @@ -0,0 +1,141 @@ +package ws + +import ( + "encoding/hex" + "math/big" + + "github.com/ethereum/go-ethereum/crypto" + "github.com/pkg/errors" + "github.com/spf13/cobra" + + "github.com/iotexproject/iotex-core/ioctl/cmd/ws/contracts" + "github.com/iotexproject/iotex-core/ioctl/config" + "github.com/iotexproject/iotex-core/ioctl/output" +) + +var ( + wsProjectAttributeCmd = &cobra.Command{ + Use: "attributes", + Short: config.TranslateInLang(map[config.Language]string{ + config.English: "project attributes operations", + config.Chinese: "项目属性配置操作", + }, config.UILanguage), + } + + wsProjectAttributeSetCmd = &cobra.Command{ + Use: "set", + Short: config.TranslateInLang(map[config.Language]string{ + config.English: "set project attributes", + config.Chinese: "配置项目属性", + }, config.UILanguage), + RunE: func(cmd *cobra.Command, args []string) error { + out, err := setAttribute( + big.NewInt(int64(projectID.Value().(uint64))), + projectAttrKey.Value().(string), + projectAttrVal.Value().(string), + ) + if err != nil { + return output.PrintError(err) + } + output.PrintResult(output.JSONString(out)) + return nil + }, + } + + wsProjectAttributeGetCmd = &cobra.Command{ + Use: "get", + Short: config.TranslateInLang(map[config.Language]string{ + config.English: "get project attributes", + config.Chinese: "获取项目属性", + }, config.UILanguage), + RunE: func(cmd *cobra.Command, args []string) error { + out, err := getAttribute( + big.NewInt(int64(projectID.Value().(uint64))), + projectAttrKey.Value().(string), + ) + if err != nil { + return output.PrintError(err) + } + output.PrintResult(output.JSONString(out)) + return nil + }, + } +) + +func init() { + projectID.RegisterCommand(wsProjectAttributeSetCmd) + projectID.MarkFlagRequired(wsProjectAttributeSetCmd) + projectAttrKey.RegisterCommand(wsProjectAttributeSetCmd) + projectAttrKey.MarkFlagRequired(wsProjectAttributeSetCmd) + projectAttrVal.RegisterCommand(wsProjectAttributeSetCmd) + projectAttrVal.MarkFlagRequired(wsProjectAttributeSetCmd) + + projectID.RegisterCommand(wsProjectAttributeGetCmd) + projectID.MarkFlagRequired(wsProjectAttributeGetCmd) + projectAttrKey.RegisterCommand(wsProjectAttributeGetCmd) + projectAttrKey.MarkFlagRequired(wsProjectAttributeGetCmd) + + wsProjectAttributeCmd.AddCommand(wsProjectAttributeSetCmd) + wsProjectAttributeCmd.AddCommand(wsProjectAttributeGetCmd) + + wsProject.AddCommand(wsProjectAttributeCmd) +} + +func getAttribute(projectID *big.Int, key string) (any, error) { + caller, err := NewContractCaller(projectStoreABI, projectStoreAddress) + if err != nil { + return nil, errors.Wrap(err, "failed to create contract caller") + } + result := NewContractResult(&projectStoreABI, funcGetProjectAttr, new([]byte)) + keysig := crypto.Keccak256Hash([]byte(key)) + if err = caller.Read(funcGetProjectAttr, []any{projectID, keysig}, result); err != nil { + return nil, errors.Wrap(err, "failed to read contract") + } + v, err := result.Result() + if err != nil { + return nil, err + } + return &struct { + ProjectID uint64 `json:"projectID"` + Key string `json:"key"` + KeySig string `json:"keySig"` + Val string `json:"val"` + }{ + ProjectID: projectID.Uint64(), + Key: key, + KeySig: hex.EncodeToString(keysig[:]), + Val: string(*v.(*[]byte)), + }, nil +} + +func setAttribute(projectID *big.Int, key, val string) (any, error) { + caller, err := NewContractCaller(projectStoreABI, projectStoreAddress) + if err != nil { + return nil, errors.Wrap(err, "failed to create contract caller") + } + + value := new(contracts.W3bstreamProjectAttributeSet) + result := NewContractResult(&projectStoreABI, eventProjectAttrSet, value) + keysig := crypto.Keccak256Hash([]byte(key)) + if _, err = caller.CallAndRetrieveResult(funcSetProjectAttr, []any{ + projectID, + [][32]byte{keysig}, + [][]byte{[]byte(val)}, + }, result); err != nil { + return nil, errors.Wrap(err, "failed to read contract") + } + if _, err = result.Result(); err != nil { + return nil, err + } + return &struct { + ProjectID uint64 `json:"projectID"` + Key string `json:"key"` + KeySig string `json:"keySig"` + Val string `json:"val"` + }{ + ProjectID: projectID.Uint64(), + Key: key, + KeySig: hex.EncodeToString(value.Key[:]), + Val: string(value.Value), + }, nil +} diff --git a/ioctl/cmd/ws/wsprojectconfig.go b/ioctl/cmd/ws/wsprojectconfig.go index f67cb81b0e..c35c42258a 100644 --- a/ioctl/cmd/ws/wsprojectconfig.go +++ b/ioctl/cmd/ws/wsprojectconfig.go @@ -117,6 +117,8 @@ func init() { _ = wsProjectConfig.MarkFlagRequired("data-source") _ = wsProjectConfig.MarkFlagRequired("vm-type") _ = wsProjectConfig.MarkFlagRequired("code-file") + + wsProject.AddCommand(wsProjectConfig) } type projectConfig struct { diff --git a/ioctl/cmd/ws/wsprojectcreate.go b/ioctl/cmd/ws/wsprojectcreate.go deleted file mode 100644 index bbea599b3b..0000000000 --- a/ioctl/cmd/ws/wsprojectcreate.go +++ /dev/null @@ -1,106 +0,0 @@ -package ws - -import ( - "encoding/hex" - "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 { - filename, err := cmd.Flags().GetString("project-config-file") - if err != nil { - return output.PrintError(err) - } - hash, err := cmd.Flags().GetString("project-config-hash") - if err != nil { - return output.PrintError(err) - } - out, err := createProject(filename, 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: "创建项目", - } - - _flagProjectConfigFileUsages = map[config.Language]string{ - config.English: "project config file path", - config.Chinese: "项目配置文件路径", - } - _flagProjectConfigHashUsages = map[config.Language]string{ - config.English: "project config file hash(sha256) for validating", - config.Chinese: "项目配置文件sha256哈希", - } -) - -func init() { - wsProjectCreate.Flags().StringP("project-config-file", "u", "", config.TranslateInLang(_flagProjectConfigFileUsages, config.UILanguage)) - wsProjectCreate.Flags().StringP("project-config-hash", "v", "", config.TranslateInLang(_flagProjectConfigHashUsages, config.UILanguage)) - - _ = wsProjectCreate.MarkFlagRequired("project-config-file") -} - -func createProject(filename, hashstr string) (string, error) { - uri, hashv, err := upload(wsProjectIPFSEndpoint, filename, hashstr) - if err != nil { - return "", err - } - fmt.Printf("project config file validated and uploaded:\n"+ - "\tipfs uri: %s\n"+ - "\thash256: %s\n\n\n", uri, hex.EncodeToString(hashv[:])) - - contract, err := util.Address(wsProjectRegisterContractAddress) - if err != nil { - return "", output.NewError(output.AddressError, "failed to get project register contract address", err) - } - - bytecode, err := wsProjectRegisterContractABI.Pack(createWsProjectFuncName, uri, hashv) - 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, wsProjectUpsertedEventName) - 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") - } - hashstr = hex.EncodeToString(hashv[:]) - return fmt.Sprintf("Your project is successfully created:\n%s", output.JSONString(&projectMeta{ - ProjectID: projectid.(uint64), - URI: uri, - HashSha256: hashstr, - })), nil -} diff --git a/ioctl/cmd/ws/wsprojectquery.go b/ioctl/cmd/ws/wsprojectquery.go index 57d76162a7..ab58271d8a 100644 --- a/ioctl/cmd/ws/wsprojectquery.go +++ b/ioctl/cmd/ws/wsprojectquery.go @@ -1,66 +1,93 @@ package ws import ( - "fmt" + "encoding/hex" + "math/big" - "github.com/iotexproject/iotex-address/address" + "github.com/ethereum/go-ethereum/common" "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/cmd/ws/contracts" "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{ +var wsProjectQueryCmd = &cobra.Command{ + Use: "query", + Short: config.TranslateInLang(map[config.Language]string{ config.English: "query w3bstream project", config.Chinese: "查询项目", - } -) + }, config.UILanguage), + RunE: func(cmd *cobra.Command, args []string) error { + out, err := queryProject(big.NewInt(int64(projectID.Value().(uint64)))) + if err != nil { + return output.PrintError(err) + } + output.PrintResult(output.JSONString(out)) + return nil + }, +} func init() { - wsProjectQuery.Flags().Uint64P("project-id", "i", 0, config.TranslateInLang(_flagProjectIDUsages, config.UILanguage)) + projectID.RegisterCommand(wsProjectQueryCmd) + projectID.MarkFlagRequired(wsProjectQueryCmd) - _ = wsProjectQuery.MarkFlagRequired("project-id") + wsProject.AddCommand(wsProjectQueryCmd) } -func queryProject(id uint64) (string, error) { - contractAddr, err := util.Address(wsProjectRegisterContractAddress) +func queryProject(projectID *big.Int) (any, error) { + caller, err := NewContractCaller(projectStoreABI, projectStoreAddress) + if err != nil { + return nil, errors.Wrap(err, "failed to new contract caller") + } + + result := NewContractResult(&projectStoreABI, funcQueryProject, new(contracts.W3bstreamProjectProjectConfig)) + if err = caller.Read(funcQueryProject, []any{projectID}, result); err != nil { + return nil, errors.Wrapf(err, "failed to read contract: %s", funcQueryProject) + } + value, err := result.Result() + if err != nil { + return nil, err + } + + result = NewContractResult(&projectStoreABI, funcIsProjectPaused, new(bool)) + if err = caller.Read(funcIsProjectPaused, []any{projectID}, result); err != nil { + return nil, errors.Wrapf(err, "failed to read contract: %s", funcQueryProject) + } + isPaused, err := result.Result() if err != nil { - return "", output.NewError(output.AddressError, "failed to get project register contract address", err) + return nil, err } - bytecode, err := wsProjectRegisterContractABI.Pack(queryWsProjectFuncName, id) + result = NewContractResult(&projectStoreABI, funcQueryProjectOwner, new(common.Address)) + if err = caller.Read(funcQueryProjectOwner, []any{projectID}, result); err != nil { + return nil, errors.Wrapf(err, "failed to read contract: %s", funcQueryProject) + } + owner, err := result.Result() if err != nil { - return "", output.NewError(output.ConvertError, fmt.Sprintf("failed to pack abi"), err) + return nil, err } - addr, _ := address.FromString(contractAddr) - data, err := action.Read(addr, "0", bytecode) + ownerAddr, err := util.Address((*(owner.(*common.Address))).String()) if err != nil { - return "", errors.Wrap(err, "read contract failed") + return nil, errors.Wrap(err, "failed to convert owner address") } - return contract.ParseOutput(&wsProjectRegisterContractABI, queryWsProjectFuncName, data) + + _projectConfig := value.(*contracts.W3bstreamProjectProjectConfig) + + return &struct { + ProjectID uint64 `json:"projectID"` + Owner string `json:"owner"` + URI string `json:"uri"` + Hash string `json:"hash"` + IsPaused bool `json:"isPaused"` + }{ + ProjectID: projectID.Uint64(), + Owner: ownerAddr, + URI: _projectConfig.Uri, + Hash: hex.EncodeToString(_projectConfig.Hash[:]), + IsPaused: *(isPaused.(*bool)), + }, nil } diff --git a/ioctl/cmd/ws/wsprojectregister.go b/ioctl/cmd/ws/wsprojectregister.go new file mode 100644 index 0000000000..095c662d38 --- /dev/null +++ b/ioctl/cmd/ws/wsprojectregister.go @@ -0,0 +1,59 @@ +package ws + +import ( + "math/big" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + + "github.com/iotexproject/iotex-core/ioctl/cmd/ws/contracts" + "github.com/iotexproject/iotex-core/ioctl/config" + "github.com/iotexproject/iotex-core/ioctl/output" +) + +var wsProjectRegisterCmd = &cobra.Command{ + Use: "register", + Short: config.TranslateInLang(map[config.Language]string{ + config.English: "register w3bstream project", + config.Chinese: "创建w3bstream项目", + }, config.UILanguage), + RunE: func(cmd *cobra.Command, args []string) error { + out, err := registerProject(int64(projectID.Value().(uint64))) + if err != nil { + return output.PrintError(err) + } + output.PrintResult(output.JSONString(out)) + return nil + }, +} + +func init() { + projectID.RegisterCommand(wsProjectRegisterCmd) + _ = wsProjectRegisterCmd.MarkFlagRequired(projectID.Label()) + + transferAmount.RegisterCommand(wsProjectRegisterCmd) + + wsProject.AddCommand(wsProjectRegisterCmd) +} + +func registerProject(projectID int64) (any, error) { + caller, err := NewContractCaller(projectRegistrarABI, projectRegistrarAddr) + if err != nil { + return nil, errors.Wrap(err, "failed to create contract caller") + } + caller.SetAmount(big.NewInt(int64(transferAmount.Value().(uint64)))) + + result := NewContractResult(&projectStoreABI, eventOnProjectRegistered, new(contracts.W3bstreamProjectProjectBinded)) + + _, err = caller.CallAndRetrieveResult(funcProjectRegister, []any{big.NewInt(projectID)}, result) + if err != nil { + return nil, errors.Wrapf(err, "failed to call contract: %s.%s", projectRegistrarAddr, funcProjectRegister) + } + + v, err := result.Result() + if err != nil { + return nil, err + } + + return queryProject(v.(*contracts.W3bstreamProjectProjectBinded).ProjectId) +} diff --git a/ioctl/cmd/ws/wsprojectstate.go b/ioctl/cmd/ws/wsprojectstate.go new file mode 100644 index 0000000000..1aea8f21c9 --- /dev/null +++ b/ioctl/cmd/ws/wsprojectstate.go @@ -0,0 +1,95 @@ +package ws + +import ( + "fmt" + "math/big" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + + "github.com/iotexproject/iotex-core/ioctl/config" + "github.com/iotexproject/iotex-core/ioctl/output" +) + +var wsProjectPauseCmd = &cobra.Command{ + Use: "pause", + Short: config.TranslateInLang(map[config.Language]string{ + config.English: "pause project", + config.Chinese: "停止项目", + }, config.UILanguage), + RunE: func(cmd *cobra.Command, args []string) error { + projectID := big.NewInt(int64(projectID.Value().(uint64))) + out, err := pauseProject(projectID) + if err != nil { + return output.PrintError(err) + } + output.PrintResult(fmt.Sprintf("project %d paused", projectID)) + output.PrintResult(output.JSONString(out)) + return nil + }, +} + +var wsProjectResumeCmd = &cobra.Command{ + Use: "resume", + Short: config.TranslateInLang(map[config.Language]string{ + config.English: "resume project", + config.Chinese: "开启项目", + }, config.UILanguage), + RunE: func(cmd *cobra.Command, args []string) error { + projectID := big.NewInt(int64(projectID.Value().(uint64))) + out, err := resumeProject(projectID) + if err != nil { + return output.PrintError(err) + } + output.PrintResult(fmt.Sprintf("project %d resumed", projectID)) + output.PrintResult(output.JSONString(out)) + return nil + }, +} + +func init() { + projectID.RegisterCommand(wsProjectPauseCmd) + projectID.MarkFlagRequired(wsProjectPauseCmd) + + projectID.RegisterCommand(wsProjectResumeCmd) + projectID.MarkFlagRequired(wsProjectResumeCmd) + + wsProject.AddCommand(wsProjectPauseCmd) + wsProject.AddCommand(wsProjectResumeCmd) +} + +func pauseProject(projectID *big.Int) (any, error) { + caller, err := NewContractCaller(projectStoreABI, projectStoreAddress) + if err != nil { + return nil, errors.Wrap(err, "failed to new contract caller") + } + + result := NewContractResult(&projectStoreABI, eventProjectPaused, nil) + _, err = caller.CallAndRetrieveResult(funcPauseProject, []any{projectID}, result) + if err != nil { + return nil, errors.Wrap(err, "failed to call contract") + } + _, err = result.Result() + if err != nil { + return nil, err + } + return queryProject(projectID) +} + +func resumeProject(projectID *big.Int) (any, error) { + caller, err := NewContractCaller(projectStoreABI, projectStoreAddress) + if err != nil { + return nil, errors.Wrap(err, "failed to new contract caller") + } + + result := NewContractResult(&projectStoreABI, eventProjectResumed, nil) + _, err = caller.CallAndRetrieveResult(funcResumeProject, []any{projectID}, result) + if err != nil { + return nil, errors.Wrap(err, "failed to call contract") + } + _, err = result.Result() + if err != nil { + return nil, err + } + return queryProject(projectID) +} diff --git a/ioctl/cmd/ws/wsprojectupdate.go b/ioctl/cmd/ws/wsprojectupdate.go index b2cab1ae20..bcb08db60a 100644 --- a/ioctl/cmd/ws/wsprojectupdate.go +++ b/ioctl/cmd/ws/wsprojectupdate.go @@ -1,111 +1,68 @@ package ws import ( - "encoding/hex" - "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/cmd/ws/contracts" "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) - } - filename, err := cmd.Flags().GetString("project-config-file") - if err != nil { - return output.PrintError(err) - } - hash, err := cmd.Flags().GetString("project-config-hash") - if err != nil { - return output.PrintError(err) - } - out, err := updateProject(id, filename, hash) - if err != nil { - return output.PrintError(err) - } - output.PrintResult(out) - return nil - }, - } - - // wsProjectUpdateShorts update w3bstream project shorts multi-lang support - wsProjectUpdateShorts = map[config.Language]string{ +var wsProjectUpdateCmd = &cobra.Command{ + Use: "update", + Short: config.TranslateInLang(map[config.Language]string{ config.English: "update w3bstream project", config.Chinese: "更新项目", - } -) + }, config.UILanguage), + RunE: func(cmd *cobra.Command, args []string) error { + out, err := updateProject( + big.NewInt(int64(projectID.Value().(uint64))), + projectFilePath.Value().(string), + projectFileHash.Value().(string), + ) + if err != nil { + return output.PrintError(err) + } + output.PrintResult("project updated") + output.PrintResult(output.JSONString(out)) + return nil + }, +} func init() { - wsProjectUpdate.Flags().Uint64P("project-id", "i", 0, config.TranslateInLang(_flagProjectIDUsages, config.UILanguage)) - wsProjectUpdate.Flags().StringP("project-config-file", "u", "", config.TranslateInLang(_flagProjectConfigFileUsages, config.UILanguage)) - wsProjectUpdate.Flags().StringP("project-config-hash", "v", "", config.TranslateInLang(_flagProjectConfigHashUsages, config.UILanguage)) + projectID.RegisterCommand(wsProjectUpdateCmd) + projectID.MarkFlagRequired(wsProjectUpdateCmd) + projectFilePath.RegisterCommand(wsProjectUpdateCmd) + projectFilePath.MarkFlagRequired(wsProjectUpdateCmd) + projectFileHash.RegisterCommand(wsProjectUpdateCmd) - _ = wsProjectCreate.MarkFlagRequired("project-id") - _ = wsProjectCreate.MarkFlagRequired("project-config-file") + wsProject.AddCommand(wsProjectUpdateCmd) } -func updateProject(projectID uint64, filename, hashstr string) (string, error) { - uri, hashv, err := upload(wsProjectIPFSEndpoint, filename, hashstr) - if err != nil { - return "", err - } - fmt.Printf("project config file validated and uploaded:\n"+ - "\tipfs uri: %s\n"+ - "\thash256: %s\n\n\n", uri, hex.EncodeToString(hashv[:])) - - contract, err := util.Address(wsProjectRegisterContractAddress) - if err != nil { - return "", output.NewError(output.AddressError, "failed to get project register contract address", err) - } - - bytecode, err := wsProjectRegisterContractABI.Pack(updateWsProjectFuncName, projectID, uri, hashv) +func updateProject(projectID *big.Int, filename, hashstr string) (any, error) { + uri, hashval, err := uploadToIPFS(ipfsEndpoint, filename, hashstr) if err != nil { - return "", output.NewError(output.ConvertError, fmt.Sprintf("failed to pack abi"), err) + return nil, errors.Wrap(err, "failed to upload project file to ipfs") } - res, err := action.ExecuteAndResponse(contract, big.NewInt(0), bytecode) + caller, err := NewContractCaller(projectStoreABI, projectStoreAddress) if err != nil { - return "", errors.Wrap(err, "execute contract failed") + return nil, errors.Wrap(err, "failed to create contract caller") } - r, err := waitReceiptByActionHash(res.ActionHash) + result := NewContractResult(&projectStoreABI, eventProjectConfigUpdated, new(contracts.W3bstreamProjectProjectConfigUpdated)) + _, err = caller.CallAndRetrieveResult(funcUpdateProjectConfig, []any{projectID, uri, hashval}, result) if err != nil { - return "", errors.Wrap(err, "wait contract execution receipt failed") + return nil, errors.Wrap(err, "failed to update project config") } - inputs, err := getEventInputsByName(r.ReceiptInfo.Receipt.Logs, wsProjectUpsertedEventName) + _v, err := result.Result() if err != nil { - return "", errors.Wrap(err, "get receipt event failed") - } - _projectid, ok := inputs["projectId"] - if !ok { - return "", errors.New("result `projectId` not found in event inputs") - } - _uri, ok := inputs["uri"] - if !ok { - return "", errors.New("result `uri` not found in event inputs") - } - _hash, ok := inputs["hash"].([32]byte) - if !ok { - return "", errors.New("result `hash` not found in event inputs") + return nil, err } - hashstr = hex.EncodeToString(_hash[:]) - return fmt.Sprintf("Your project is successfully updated:\n%s", output.JSONString(projectMeta{ - ProjectID: _projectid.(uint64), - URI: _uri.(string), - HashSha256: hashstr, - })), nil + v := _v.(*contracts.W3bstreamProjectProjectConfigUpdated) + return queryProject(v.ProjectId) } diff --git a/ioctl/cmd/ws/wsutil.go b/ioctl/cmd/ws/wsutil.go new file mode 100644 index 0000000000..a578e55c32 --- /dev/null +++ b/ioctl/cmd/ws/wsutil.go @@ -0,0 +1,403 @@ +package ws + +import ( + "bytes" + "context" + "encoding/hex" + "math/big" + "time" + + "github.com/cenkalti/backoff" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/grpc-ecosystem/go-grpc-middleware/util/metautils" + "github.com/iotexproject/iotex-address/address" + "github.com/iotexproject/iotex-proto/golang/iotexapi" + "github.com/iotexproject/iotex-proto/golang/iotextypes" + "github.com/pkg/errors" + "google.golang.org/grpc" + + "github.com/iotexproject/iotex-core/action" + "github.com/iotexproject/iotex-core/ioctl/cmd/account" + "github.com/iotexproject/iotex-core/ioctl/config" + "github.com/iotexproject/iotex-core/ioctl/util" +) + +func NewContractCaller(contractabi abi.ABI, contractaddress string) (*ContractCaller, error) { + caller := &ContractCaller{ + abi: contractabi, + wait: time.Second * 10, + backoff: 3, + ctx: context.Background(), + amount: big.NewInt(0), + } + + // read current account from global config + addr, err := config.GetContextAddressOrAlias() + if err != nil { + return nil, errors.Wrap(err, "failed to get current account") + } + sender, err := util.Address(addr) + if err != nil { + return nil, errors.Wrapf(err, "failed to get current account address: %s", addr) + } + caller.sender, err = address.FromString(sender) + if err != nil { + return nil, errors.Wrap(err, "failed to get current account") + } + + // contract address + addr, err = util.Address(contractaddress) + if err != nil { + return nil, errors.Wrapf(err, "failed to parse contract address: %s", contractaddress) + } + caller.contract, err = address.FromString(addr) + if err != nil { + return nil, errors.Wrapf(err, "failed to parse contract address: %s", contractaddress) + } + + // initialize api client + caller.conn, err = util.ConnectToEndpoint(config.ReadConfig.SecureConnect && !config.Insecure) + if err != nil { + return nil, errors.Wrapf(err, "failed to connect to chain endpoint: %s", config.ReadConfig.Endpoint) + } + + caller.client = iotexapi.NewAPIServiceClient(caller.conn) + if metadata, err := util.JwtAuth(); err == nil { + caller.ctx = metautils.NiceMD(metadata).ToOutgoing(caller.ctx) + } + + // read chain id once + response, err := caller.client.GetChainMeta(caller.ctx, &iotexapi.GetChainMetaRequest{}) + if err != nil { + return nil, errors.Wrap(err, "failed to get chain meta") + } + caller.chainID = response.GetChainMeta().GetChainID() + return caller, nil +} + +func NewContractCallerByABI(content []byte, address string) (*ContractCaller, error) { + _abi, err := abi.JSON(bytes.NewBuffer(content)) + if err != nil { + return nil, errors.Wrap(err, "failed to parse abi content") + } + return NewContractCaller(_abi, address) +} + +type ContractCaller struct { + sender address.Address // sender as contract caller + password string // password debug only + client iotexapi.APIServiceClient // client api service client + conn *grpc.ClientConn // conn grpc connection + chainID uint32 // chainID responding global chain endpoint + ctx context.Context // ctx api invoke context + abi abi.ABI // abi contract abi + contract address.Address // contract address + wait time.Duration // wait wait duration + backoff uint64 // backoff times + amount *big.Int // amount for tx amount +} + +func (c *ContractCaller) SetAmount(amount *big.Int) { + c.amount = amount +} + +func (c *ContractCaller) Sender() address.Address { + return c.sender +} + +func (c *ContractCaller) Close() { + _ = c.conn.Close() +} + +func (c *ContractCaller) SetPassword(password string) { + c.password = password +} + +func (c *ContractCaller) IsSender(cmp common.Address) bool { + return bytes.Equal(c.sender.Bytes(), cmp.Bytes()) +} + +func (c *ContractCaller) Read(method string, arguments []any, result *ContractResult) error { + bytecode, err := c.abi.Pack(method, arguments...) + if err != nil { + return errors.Wrapf(err, "failed to pack method: %s", method) + } + + response, err := c.client.ReadContract(c.ctx, &iotexapi.ReadContractRequest{ + Execution: &iotextypes.Execution{ + Amount: "0", + Contract: c.contract.String(), + Data: bytecode, + }, + CallerAddress: c.sender.String(), + }) + if err != nil { + return errors.Wrapf(err, "failed to read contract: %s", method) + } + + if result != nil { + result.parseOutput(response.Data) + } + return nil +} + +func (c *ContractCaller) envelop(method string, arguments ...any) (action.Envelope, error) { + bytecode, err := c.abi.Pack(method, arguments...) + if err != nil { + return nil, errors.Wrapf(err, "failed to pack method: %s", method) + } + tx, err := action.NewExecution(c.contract.String(), 0, c.amount, 0, big.NewInt(0), bytecode) + if err != nil { + return nil, errors.Wrap(err, "failed to new execution") + } + + gasPrice, err := c.gasPrice() + if err != nil { + return nil, err + } + tx.SetGasPrice(gasPrice) + + meta, err := c.accountMeta() + if err != nil { + return nil, errors.Wrap(err, "failed to get account meta") + } + tx.SetNonce(meta.GetPendingNonce()) + + gasLimit, err := c.gasLimit(tx.Proto()) + if err != nil { + return nil, err + // gasLimit = 20000000 + } + tx.SetGasLimit(gasLimit) + + balance, ok := new(big.Int).SetString(meta.GetBalance(), 10) + if !ok { + return nil, errors.Errorf("failed to convert balance: %s", meta.GetBalance()) + } + cost, _ := tx.Cost() + if balance.Cmp(cost) < 0 { + return nil, errors.Errorf("balance is not enouth: %s", meta.GetAddress()) + } + + envelop := (&action.EnvelopeBuilder{}). + SetNonce(tx.Nonce()). + SetGasPrice(tx.GasPrice()). + SetGasLimit(tx.GasLimit()). + SetAction(tx). + SetChainID(c.chainID). + Build() + + return envelop, nil +} + +func (c *ContractCaller) gasPrice() (*big.Int, error) { + response, err := c.client.SuggestGasPrice(c.ctx, &iotexapi.SuggestGasPriceRequest{}) + if err != nil { + return nil, errors.Wrap(err, "failed to get suggest gas price") + } + return new(big.Int).SetUint64(response.GetGasPrice()), nil +} + +func (c *ContractCaller) gasLimit(tx *iotextypes.Execution) (uint64, error) { + response, err := c.client.EstimateActionGasConsumption(c.ctx, &iotexapi.EstimateActionGasConsumptionRequest{ + Action: &iotexapi.EstimateActionGasConsumptionRequest_Execution{ + Execution: tx, + }, + CallerAddress: c.sender.String(), + }) + if err != nil { + return 0, errors.Wrap(err, "failed to estimate action gas consumption") + } + return response.GetGas(), nil +} + +func (c *ContractCaller) accountMeta() (*iotextypes.AccountMeta, error) { + response, err := c.client.GetAccount(c.ctx, &iotexapi.GetAccountRequest{ + Address: c.sender.String(), + }) + if err != nil { + return nil, errors.Wrapf(err, "failed to get account meta: %s", c.sender) + } + return response.GetAccountMeta(), nil +} + +func (c *ContractCaller) Call(method string, arguments ...any) (string, error) { + envelop, err := c.envelop(method, arguments...) + if err != nil { + return "", errors.Wrap(err, "failed to build envelop") + } + + sk, err := account.PrivateKeyFromSigner(c.sender.String(), c.password) + if err != nil { + return "", errors.Wrapf(err, "failed to get private key from %s", c.sender) + } + + sealed, err := action.Sign(envelop, sk) + if err != nil { + return "", errors.Wrap(err, "failed to sign action") + } + sk.Zero() + + response, err := c.client.SendAction(c.ctx, &iotexapi.SendActionRequest{Action: sealed.Proto()}) + if err != nil { + return "", errors.Wrap(err, "failed to send action") + } + return response.ActionHash, nil +} + +// CallAndRetrieveResult will call contract with `method` and `arguments` and parse result from `targetABI` and `eventName` +func (c *ContractCaller) CallAndRetrieveResult(method string, arguments []any, results ...*ContractResult) (string, error) { + tx, err := c.Call(method, arguments...) + if err != nil { + return "", err + } + + if len(results) == 0 { + return tx, nil + } + + var receipt *iotexapi.GetReceiptByActionResponse + err = backoff.Retry(func() error { + receipt, err = c.client.GetReceiptByAction(c.ctx, &iotexapi.GetReceiptByActionRequest{ActionHash: tx}) + if err != nil { + return err + } + return nil + }, backoff.WithMaxRetries(backoff.NewConstantBackOff(c.wait), c.backoff)) + + if err != nil { + return "", errors.Wrapf(err, "failed to query transaction [tx: %s]", tx) + } + if code := receipt.ReceiptInfo.Receipt.Status; code != uint64(iotextypes.ReceiptStatus_Success) { + return "", errors.Errorf("contract call not executed success: [tx: %s] [code: %d]", tx, code) + } + + for _, result := range results { + result.parseEvent(receipt.ReceiptInfo.Receipt.Logs) + } + return tx, nil +} + +func NewContractResult(target *abi.ABI, name string, value any) *ContractResult { + r := &ContractResult{ + target: target, + value: value, + } + + _event, ok := target.Events[name] + if ok { + r.event = &_event + r.sig = crypto.Keccak256Hash([]byte(_event.Sig)) + } + _method, ok := target.Methods[name] + if ok { + r.method = &_method + } + if r.method == nil && r.event == nil { + r.err = errors.Errorf("not fount event or method from target abi: %s", name) + } + return r +} + +type ContractResult struct { + value any + err error + target *abi.ABI + event *abi.Event + method *abi.Method + sig common.Hash +} + +func (r *ContractResult) parseOutput(data string) { + if r.err != nil { + return + } + + if r.method == nil { + r.err = errors.Errorf("not found method from target abi") + return + } + + raw, err := hex.DecodeString(data) + if err != nil { + r.err = errors.Wrap(err, "failed to decode hex string") + return + } + res, err := r.target.Unpack(r.method.Name, raw) + if err != nil { + r.err = errors.Wrap(err, "failed to unpack method") + return + } + + defer func() { + if err := recover(); err != nil { + r.err = errors.Errorf("failed to convert from %T to %T", res[0], r.value) + } + }() + + r.value = abi.ConvertType(res[0], r.value) +} + +func (r *ContractResult) parseEvent(logs []*iotextypes.Log) { + if r.err != nil { + return + } + + if r.event == nil { + r.err = errors.Errorf("not found event from target abi") + return + } + + var log *iotextypes.Log + for _, l := range logs { + for _, t := range l.Topics { + if bytes.Equal(r.sig[:], t) { + log = l + break + } + } + } + + // found event log + if log == nil { + r.err = errors.Errorf("target event not fount from logs") + return + } + + // if value is not assigned skip parsing + if r.value == nil { + return + } + + if err := r.target.UnpackIntoInterface(r.value, r.event.Name, log.Data); err != nil { + r.err = errors.Wrap(err, "failed to unpack target event data") + return + } + var indexed abi.Arguments + for _, argument := range r.event.Inputs { + if argument.Indexed { + indexed = append(indexed, argument) + } + } + + if len(log.Topics) > 1 { + topics := make([]common.Hash, 0) + for _, t := range log.Topics[1:] { + topics = append(topics, common.BytesToHash(t)) + } + + if err := abi.ParseTopics(r.value, indexed, topics); err != nil { + r.err = errors.Wrap(err, "failed to parse topic data") + return + } + } +} + +func (r *ContractResult) Result() (any, error) { + if r.err != nil { + return nil, r.err + } + return r.value, nil +}