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
This commit is contained in:
Alexander Peters
2020-06-05 15:08:11 +02:00
committed by GitHub
parent 9a16d583d3
commit ebac9aac66
9 changed files with 229 additions and 17 deletions

View File

@@ -50,7 +50,8 @@ Base64 encoded transactions.
- `raw-bytes` convert raw-bytes to hex - `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 * (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) [\#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 ## [v2.0.3] - 2019-11-04

View File

@@ -98,7 +98,7 @@ type (
Model = types.Model Model = types.Model
CodeInfo = types.CodeInfo CodeInfo = types.CodeInfo
ContractInfo = types.ContractInfo ContractInfo = types.ContractInfo
CreatedAt = types.CreatedAt CreatedAt = types.AbsoluteTxPosition
WasmConfig = types.WasmConfig WasmConfig = types.WasmConfig
MessageHandler = keeper.MessageHandler MessageHandler = keeper.MessageHandler
BankEncoder = keeper.BankEncoder BankEncoder = keeper.BankEncoder

View File

@@ -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
}

View File

@@ -14,6 +14,7 @@ import (
"github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types" 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"
"github.com/cosmos/cosmos-sdk/x/auth/client/utils" "github.com/cosmos/cosmos-sdk/x/auth/client/utils"
@@ -27,6 +28,8 @@ const (
flagSource = "source" flagSource = "source"
flagBuilder = "builder" flagBuilder = "builder"
flagLabel = "label" flagLabel = "label"
flagAdmin = "admin"
flagNoAdmin = "no-admin"
) )
// GetTxCmd returns the transaction commands for this module // GetTxCmd returns the transaction commands for this module
@@ -42,6 +45,8 @@ func GetTxCmd(cdc *codec.Codec) *cobra.Command {
StoreCodeCmd(cdc), StoreCodeCmd(cdc),
InstantiateContractCmd(cdc), InstantiateContractCmd(cdc),
ExecuteContractCmd(cdc), ExecuteContractCmd(cdc),
MigrateContractCmd(cdc),
UpdateContractAdminCmd(cdc),
)...) )...)
return txCmd return txCmd
} }
@@ -106,7 +111,7 @@ func InstantiateContractCmd(cdc *codec.Codec) *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "instantiate [code_id_int64] [json_encoded_init_args]", Use: "instantiate [code_id_int64] [json_encoded_init_args]",
Short: "Instantiate a wasm contract", Short: "Instantiate a wasm contract",
Args: cobra.ExactArgs(2), Args: cobra.RangeArgs(2, 3),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
inBuf := bufio.NewReader(cmd.InOrStdin()) inBuf := bufio.NewReader(cmd.InOrStdin())
txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc)) txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc))
@@ -131,6 +136,15 @@ func InstantiateContractCmd(cdc *codec.Codec) *cobra.Command {
initMsg := args[1] 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 // build and sign the transaction, then broadcast to Tendermint
msg := types.MsgInstantiateContract{ msg := types.MsgInstantiateContract{
Sender: cliCtx.GetFromAddress(), Sender: cliCtx.GetFromAddress(),
@@ -138,6 +152,7 @@ func InstantiateContractCmd(cdc *codec.Codec) *cobra.Command {
Label: label, Label: label,
InitFunds: amount, InitFunds: amount,
InitMsg: []byte(initMsg), InitMsg: []byte(initMsg),
Admin: adminAddr,
} }
return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg}) 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(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(flagLabel, "", "A human-readable name for this contract in lists")
cmd.Flags().String(flagAdmin, "", "Address of an admin")
return cmd return cmd
} }

View File

@@ -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})
}
}

View File

@@ -10,4 +10,5 @@ import (
func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router) { func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router) {
registerQueryRoutes(cliCtx, r) registerQueryRoutes(cliCtx, r)
registerTxRoutes(cliCtx, r) registerTxRoutes(cliCtx, r)
registerNewTxRoutes(cliCtx, r)
} }

View File

@@ -29,9 +29,10 @@ type storeCodeReq struct {
} }
type instantiateContractReq struct { type instantiateContractReq struct {
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"` BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
Deposit sdk.Coins `json:"deposit" yaml:"deposit"` Deposit sdk.Coins `json:"deposit" yaml:"deposit"`
InitMsg []byte `json:"init_msg" yaml:"init_msg"` Admin sdk.AccAddress `json:"admin,omitempty" yaml:"admin"`
InitMsg []byte `json:"init_msg" yaml:"init_msg"`
} }
type executeContractReq struct { type executeContractReq struct {
@@ -117,6 +118,7 @@ func instantiateContractHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
Code: codeID, Code: codeID,
InitFunds: req.Deposit, InitFunds: req.Deposit,
InitMsg: req.InitMsg, InitMsg: req.InitMsg,
Admin: req.Admin,
} }
err = msg.ValidateBasic() err = msg.ValidateBasic()

View File

@@ -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) // redact the Created field (just used for sorting, not part of public API)
info.Created = nil info.Created = nil
info.LastUpdated = nil
info.PreviousCodeID = 0
infoWithAddress := ContractInfoWithAddress{ infoWithAddress := ContractInfoWithAddress{
Address: addr, Address: addr,
@@ -104,7 +106,7 @@ func queryContractListByCode(ctx sdk.Context, codeIDstr string, req abci.Request
return false return false
}) })
// now we sort them by CreatedAt // now we sort them by AbsoluteTxPosition
sort.Slice(contracts, func(i, j int) bool { sort.Slice(contracts, func(i, j int) bool {
return contracts[i].ContractInfo.Created.LessThan(contracts[j].ContractInfo.Created) return contracts[i].ContractInfo.Created.LessThan(contracts[j].ContractInfo.Created)
}) })

View File

@@ -47,10 +47,9 @@ type ContractInfo struct {
InitMsg json.RawMessage `json:"init_msg,omitempty"` InitMsg json.RawMessage `json:"init_msg,omitempty"`
// never show this in query results, just use for sorting // never show this in query results, just use for sorting
// (Note: when using json tag "-" amino refused to serialize it...) // (Note: when using json tag "-" amino refused to serialize it...)
Created *CreatedAt `json:"created,omitempty"` Created *AbsoluteTxPosition `json:"created,omitempty"`
// TODO: type CreatedAt is not an accurate name. how about renaming to BlockPosition? LastUpdated *AbsoluteTxPosition `json:"last_updated,omitempty"`
LastUpdated *CreatedAt `json:"last_updated,omitempty"` PreviousCodeID uint64 `json:"previous_code_id,omitempty"`
PreviousCodeID uint64 `json:"previous_code_id,omitempty"`
} }
func (c *ContractInfo) UpdateCodeID(ctx sdk.Context, newCodeID uint64) { 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) c.LastUpdated = NewCreatedAt(ctx)
} }
// CreatedAt can be used to sort contracts // AbsoluteTxPosition can be used to sort contracts
type CreatedAt struct { type AbsoluteTxPosition struct {
// BlockHeight is the block the contract was created at // BlockHeight is the block the contract was created at
BlockHeight int64 BlockHeight int64
// TxIndex is a monotonic counter within the block (actual transaction index, or gas consumed) // 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 // LessThan can be used to sort
func (a *CreatedAt) LessThan(b *CreatedAt) bool { func (a *AbsoluteTxPosition) LessThan(b *AbsoluteTxPosition) bool {
if a == nil { if a == nil {
return true return true
} }
@@ -79,21 +78,21 @@ func (a *CreatedAt) LessThan(b *CreatedAt) bool {
} }
// NewCreatedAt gets a timestamp from the context // 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 // we must safely handle nil gas meters
var index uint64 var index uint64
meter := ctx.BlockGasMeter() meter := ctx.BlockGasMeter()
if meter != nil { if meter != nil {
index = meter.GasConsumed() index = meter.GasConsumed()
} }
return &CreatedAt{ return &AbsoluteTxPosition{
BlockHeight: ctx.BlockHeight(), BlockHeight: ctx.BlockHeight(),
TxIndex: index, TxIndex: index,
} }
} }
// NewContractInfo creates a new instance of a given WASM contract info // 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{ return ContractInfo{
CodeID: codeID, CodeID: codeID,
Creator: creator, Creator: creator,