From ebac9aac66ebfc0d7f8178ee8091824dbba56f1d Mon Sep 17 00:00:00 2001 From: Alexander Peters Date: Fri, 5 Jun 2020 15:08:11 +0200 Subject: [PATCH] Implement CLI/REST server support for new messages (#131) * Cleanup ContractInfo type * Add admin to contract instanciation * Add cli commands for new TX * Add rest support for new TX * Update changelog * Make optional admin flag for better UX * Add flag to not accidentally clear admin on update --- CHANGELOG.md | 3 +- x/wasm/alias.go | 2 +- x/wasm/client/cli/new_tx.go | 94 ++++++++++++++++++++++++++++++ x/wasm/client/cli/tx.go | 18 +++++- x/wasm/client/rest/new_tx.go | 97 +++++++++++++++++++++++++++++++ x/wasm/client/rest/rest.go | 1 + x/wasm/client/rest/tx.go | 8 ++- x/wasm/internal/keeper/querier.go | 4 +- x/wasm/internal/types/types.go | 19 +++--- 9 files changed, 229 insertions(+), 17 deletions(-) create mode 100644 x/wasm/client/cli/new_tx.go create mode 100644 x/wasm/client/rest/new_tx.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b77ed8f5a..00c01fb393 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,7 +50,8 @@ Base64 encoded transactions. - `raw-bytes` convert raw-bytes to hex * (wasmcli) [\#191](https://github.com/cosmwasm/wasmd/pull/191) Add cmd `decode-tx`, decodes a tx from hex or base64 * (wasmd) [\#9](https://github.com/cosmwasm/wasmd/pull/9) Allow gzip data in tx body on Create -* (wasmd)[\#124](https://github.com/CosmWasm/wasmd/pull/124) Update contract admin +* (wasmd) [\#124](https://github.com/CosmWasm/wasmd/pull/124) Update contract admin +* (wasmd) [\#131](https://github.com/CosmWasm/wasmd/pull/131) Implement REST server support for new messages ## [v2.0.3] - 2019-11-04 diff --git a/x/wasm/alias.go b/x/wasm/alias.go index ae870f27b9..c29b2585e8 100644 --- a/x/wasm/alias.go +++ b/x/wasm/alias.go @@ -98,7 +98,7 @@ type ( Model = types.Model CodeInfo = types.CodeInfo ContractInfo = types.ContractInfo - CreatedAt = types.CreatedAt + CreatedAt = types.AbsoluteTxPosition WasmConfig = types.WasmConfig MessageHandler = keeper.MessageHandler BankEncoder = keeper.BankEncoder diff --git a/x/wasm/client/cli/new_tx.go b/x/wasm/client/cli/new_tx.go new file mode 100644 index 0000000000..a229d14354 --- /dev/null +++ b/x/wasm/client/cli/new_tx.go @@ -0,0 +1,94 @@ +package cli + +import ( + "bufio" + "errors" + "strconv" + + "github.com/CosmWasm/wasmd/x/wasm/internal/types" + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/cosmos/cosmos-sdk/x/auth/client/utils" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +// MigrateContractCmd will migrate a contract to a new code version +func MigrateContractCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "migrate [contract_addr_bech32] [new_code_id_int64] [json_encoded_migration_args]", + Short: "Migrate a wasm contract to a new code version", + Args: cobra.ExactArgs(3), + RunE: func(cmd *cobra.Command, args []string) error { + inBuf := bufio.NewReader(cmd.InOrStdin()) + txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContextWithInput(inBuf).WithCodec(cdc) + + contractAddr, err := sdk.AccAddressFromBech32(args[0]) + if err != nil { + return sdkerrors.Wrap(err, "contract") + } + + // get the id of the code to instantiate + codeID, err := strconv.ParseUint(args[1], 10, 64) + if err != nil { + return sdkerrors.Wrap(err, "code id") + } + + migrateMsg := args[2] + + msg := types.MsgMigrateContract{ + Sender: cliCtx.GetFromAddress(), + Contract: contractAddr, + Code: codeID, + MigrateMsg: []byte(migrateMsg), + } + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + return cmd +} + +// UpdateContractAdminCmd sets or clears an admin for a contract +func UpdateContractAdminCmd(cdc *codec.Codec) *cobra.Command { + cmd := &cobra.Command{ + Use: "set-contract-admin [contract_addr_bech32] [new_admin_addr_bech32]", + Short: "Set new admin for a contract. Can be empty to prevent further migrations", + Args: cobra.RangeArgs(1, 2), + RunE: func(cmd *cobra.Command, args []string) error { + inBuf := bufio.NewReader(cmd.InOrStdin()) + txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) + cliCtx := context.NewCLIContextWithInput(inBuf).WithCodec(cdc) + + contractAddr, err := sdk.AccAddressFromBech32(args[0]) + if err != nil { + return sdkerrors.Wrap(err, "contract") + } + var newAdmin sdk.AccAddress + if len(args) > 1 && len(args[1]) != 0 { + newAdmin, err = sdk.AccAddressFromBech32(args[1]) + if err != nil { + return sdkerrors.Wrap(err, "new admin") + } + } else { + // safety net to not accidentally clear an admin + clearAdmin := viper.GetBool(flagNoAdmin) + if !clearAdmin { + return errors.New("new admin address required or no admin flag") + } + } + + msg := types.MsgUpdateAdministrator{ + Sender: cliCtx.GetFromAddress(), + Contract: contractAddr, + NewAdmin: newAdmin, + } + return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) + }, + } + cmd.Flags().Bool(flagNoAdmin, false, "Remove admin which disables future admin updates and migrations") + return cmd +} diff --git a/x/wasm/client/cli/tx.go b/x/wasm/client/cli/tx.go index 2783f9bdf7..dab11fbf36 100644 --- a/x/wasm/client/cli/tx.go +++ b/x/wasm/client/cli/tx.go @@ -14,6 +14,7 @@ import ( "github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" "github.com/cosmos/cosmos-sdk/x/auth" "github.com/cosmos/cosmos-sdk/x/auth/client/utils" @@ -27,6 +28,8 @@ const ( flagSource = "source" flagBuilder = "builder" flagLabel = "label" + flagAdmin = "admin" + flagNoAdmin = "no-admin" ) // GetTxCmd returns the transaction commands for this module @@ -42,6 +45,8 @@ func GetTxCmd(cdc *codec.Codec) *cobra.Command { StoreCodeCmd(cdc), InstantiateContractCmd(cdc), ExecuteContractCmd(cdc), + MigrateContractCmd(cdc), + UpdateContractAdminCmd(cdc), )...) return txCmd } @@ -106,7 +111,7 @@ func InstantiateContractCmd(cdc *codec.Codec) *cobra.Command { cmd := &cobra.Command{ Use: "instantiate [code_id_int64] [json_encoded_init_args]", Short: "Instantiate a wasm contract", - Args: cobra.ExactArgs(2), + Args: cobra.RangeArgs(2, 3), RunE: func(cmd *cobra.Command, args []string) error { inBuf := bufio.NewReader(cmd.InOrStdin()) txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) @@ -131,6 +136,15 @@ func InstantiateContractCmd(cdc *codec.Codec) *cobra.Command { initMsg := args[1] + adminStr := viper.GetString(flagAdmin) + var adminAddr sdk.AccAddress + if len(adminStr) != 0 { + adminAddr, err = sdk.AccAddressFromBech32(adminStr) + if err != nil { + return sdkerrors.Wrap(err, "admin") + } + } + // build and sign the transaction, then broadcast to Tendermint msg := types.MsgInstantiateContract{ Sender: cliCtx.GetFromAddress(), @@ -138,6 +152,7 @@ func InstantiateContractCmd(cdc *codec.Codec) *cobra.Command { Label: label, InitFunds: amount, InitMsg: []byte(initMsg), + Admin: adminAddr, } return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) }, @@ -145,6 +160,7 @@ func InstantiateContractCmd(cdc *codec.Codec) *cobra.Command { cmd.Flags().String(flagAmount, "", "Coins to send to the contract during instantiation") cmd.Flags().String(flagLabel, "", "A human-readable name for this contract in lists") + cmd.Flags().String(flagAdmin, "", "Address of an admin") return cmd } diff --git a/x/wasm/client/rest/new_tx.go b/x/wasm/client/rest/new_tx.go new file mode 100644 index 0000000000..977f310314 --- /dev/null +++ b/x/wasm/client/rest/new_tx.go @@ -0,0 +1,97 @@ +package rest + +import ( + "net/http" + + "github.com/CosmWasm/wasmd/x/wasm/internal/types" + "github.com/cosmos/cosmos-sdk/client/context" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/rest" + "github.com/cosmos/cosmos-sdk/x/auth/client/utils" + "github.com/gorilla/mux" +) + +func registerNewTxRoutes(cliCtx context.CLIContext, r *mux.Router) { + r.HandleFunc("/wasm/contract/{contractAddr}/admin", setContractAdminHandlerFn(cliCtx)).Methods("PUT") + r.HandleFunc("/wasm/contract/{contractAddr}/code", migrateContractHandlerFn(cliCtx)).Methods("PUT") +} + +type migrateContractReq struct { + BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"` + Admin sdk.AccAddress `json:"admin,omitempty" yaml:"admin"` + codeID uint64 `json:"code_id" yaml:"code_id"` + MigrateMsg []byte `json:"migrate_msg,omitempty" yaml:"migrate_msg"` +} +type updateContractAdministrateReq struct { + BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"` + Admin sdk.AccAddress `json:"admin,omitempty" yaml:"admin"` +} + +func setContractAdminHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req updateContractAdministrateReq + if !rest.ReadRESTReq(w, r, cliCtx.Codec, &req) { + return + } + vars := mux.Vars(r) + contractAddr := vars["contractAddr"] + + req.BaseReq = req.BaseReq.Sanitize() + if !req.BaseReq.ValidateBasic(w) { + return + } + + contractAddress, err := sdk.AccAddressFromBech32(contractAddr) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + msg := types.MsgUpdateAdministrator{ + Sender: cliCtx.GetFromAddress(), + NewAdmin: req.Admin, + Contract: contractAddress, + } + if err = msg.ValidateBasic(); err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + utils.WriteGenerateStdTxResponse(w, cliCtx, req.BaseReq, []sdk.Msg{msg}) + } +} + +func migrateContractHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req migrateContractReq + if !rest.ReadRESTReq(w, r, cliCtx.Codec, &req) { + return + } + vars := mux.Vars(r) + contractAddr := vars["contractAddr"] + + req.BaseReq = req.BaseReq.Sanitize() + if !req.BaseReq.ValidateBasic(w) { + return + } + + contractAddress, err := sdk.AccAddressFromBech32(contractAddr) + if err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + msg := types.MsgMigrateContract{ + Sender: cliCtx.GetFromAddress(), + Contract: contractAddress, + Code: req.codeID, + MigrateMsg: req.MigrateMsg, + } + if err = msg.ValidateBasic(); err != nil { + rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) + return + } + + utils.WriteGenerateStdTxResponse(w, cliCtx, req.BaseReq, []sdk.Msg{msg}) + } +} diff --git a/x/wasm/client/rest/rest.go b/x/wasm/client/rest/rest.go index 4237d59e25..fc47da384d 100644 --- a/x/wasm/client/rest/rest.go +++ b/x/wasm/client/rest/rest.go @@ -10,4 +10,5 @@ import ( func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router) { registerQueryRoutes(cliCtx, r) registerTxRoutes(cliCtx, r) + registerNewTxRoutes(cliCtx, r) } diff --git a/x/wasm/client/rest/tx.go b/x/wasm/client/rest/tx.go index 3d06049345..93976e7f4a 100644 --- a/x/wasm/client/rest/tx.go +++ b/x/wasm/client/rest/tx.go @@ -29,9 +29,10 @@ type storeCodeReq struct { } type instantiateContractReq struct { - BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"` - Deposit sdk.Coins `json:"deposit" yaml:"deposit"` - InitMsg []byte `json:"init_msg" yaml:"init_msg"` + BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"` + Deposit sdk.Coins `json:"deposit" yaml:"deposit"` + Admin sdk.AccAddress `json:"admin,omitempty" yaml:"admin"` + InitMsg []byte `json:"init_msg" yaml:"init_msg"` } type executeContractReq struct { @@ -117,6 +118,7 @@ func instantiateContractHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { Code: codeID, InitFunds: req.Deposit, InitMsg: req.InitMsg, + Admin: req.Admin, } err = msg.ValidateBasic() diff --git a/x/wasm/internal/keeper/querier.go b/x/wasm/internal/keeper/querier.go index 788a10a2d5..6a6b799d38 100644 --- a/x/wasm/internal/keeper/querier.go +++ b/x/wasm/internal/keeper/querier.go @@ -71,6 +71,8 @@ func queryContractInfo(ctx sdk.Context, bech string, req abci.RequestQuery, keep } // redact the Created field (just used for sorting, not part of public API) info.Created = nil + info.LastUpdated = nil + info.PreviousCodeID = 0 infoWithAddress := ContractInfoWithAddress{ Address: addr, @@ -104,7 +106,7 @@ func queryContractListByCode(ctx sdk.Context, codeIDstr string, req abci.Request return false }) - // now we sort them by CreatedAt + // now we sort them by AbsoluteTxPosition sort.Slice(contracts, func(i, j int) bool { return contracts[i].ContractInfo.Created.LessThan(contracts[j].ContractInfo.Created) }) diff --git a/x/wasm/internal/types/types.go b/x/wasm/internal/types/types.go index d6587f62dd..028d867d09 100644 --- a/x/wasm/internal/types/types.go +++ b/x/wasm/internal/types/types.go @@ -47,10 +47,9 @@ type ContractInfo struct { InitMsg json.RawMessage `json:"init_msg,omitempty"` // never show this in query results, just use for sorting // (Note: when using json tag "-" amino refused to serialize it...) - Created *CreatedAt `json:"created,omitempty"` - // TODO: type CreatedAt is not an accurate name. how about renaming to BlockPosition? - LastUpdated *CreatedAt `json:"last_updated,omitempty"` - PreviousCodeID uint64 `json:"previous_code_id,omitempty"` + Created *AbsoluteTxPosition `json:"created,omitempty"` + LastUpdated *AbsoluteTxPosition `json:"last_updated,omitempty"` + PreviousCodeID uint64 `json:"previous_code_id,omitempty"` } func (c *ContractInfo) UpdateCodeID(ctx sdk.Context, newCodeID uint64) { @@ -59,8 +58,8 @@ func (c *ContractInfo) UpdateCodeID(ctx sdk.Context, newCodeID uint64) { c.LastUpdated = NewCreatedAt(ctx) } -// CreatedAt can be used to sort contracts -type CreatedAt struct { +// AbsoluteTxPosition can be used to sort contracts +type AbsoluteTxPosition struct { // BlockHeight is the block the contract was created at BlockHeight int64 // TxIndex is a monotonic counter within the block (actual transaction index, or gas consumed) @@ -68,7 +67,7 @@ type CreatedAt struct { } // LessThan can be used to sort -func (a *CreatedAt) LessThan(b *CreatedAt) bool { +func (a *AbsoluteTxPosition) LessThan(b *AbsoluteTxPosition) bool { if a == nil { return true } @@ -79,21 +78,21 @@ func (a *CreatedAt) LessThan(b *CreatedAt) bool { } // NewCreatedAt gets a timestamp from the context -func NewCreatedAt(ctx sdk.Context) *CreatedAt { +func NewCreatedAt(ctx sdk.Context) *AbsoluteTxPosition { // we must safely handle nil gas meters var index uint64 meter := ctx.BlockGasMeter() if meter != nil { index = meter.GasConsumed() } - return &CreatedAt{ + return &AbsoluteTxPosition{ BlockHeight: ctx.BlockHeight(), TxIndex: index, } } // NewContractInfo creates a new instance of a given WASM contract info -func NewContractInfo(codeID uint64, creator, admin sdk.AccAddress, initMsg []byte, label string, createdAt *CreatedAt) ContractInfo { +func NewContractInfo(codeID uint64, creator, admin sdk.AccAddress, initMsg []byte, label string, createdAt *AbsoluteTxPosition) ContractInfo { return ContractInfo{ CodeID: codeID, Creator: creator,