Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Backport: CLI enhancements: #1732, #1733, #1759, #1762, #1798. #1802

Merged
merged 6 commits into from
Jan 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,25 @@ Ref: https://keepachangelog.com/en/1.0.0/

## [Unreleased]

### Features

* Add CLI commands for the exchange module endpoints and queries [#1701](https://github.com/provenance-io/provenance/issues/1701).
* Create CLI commands for adding a market to a genesis file [#1757](https://github.com/provenance-io/provenance/issues/1757).
* Add CLI command to generate autocomplete shell scripts [#1762](https://github.com/provenance-io/provenance/pull/1762).

### Improvements

* Create a default market in `make run`, `localnet`, `devnet` and the `provenanced testnet` command [#1757](https://github.com/provenance-io/provenance/issues/1757).
* Updated documentation for each module to work with docusaurus [PR 1763](https://github.com/provenance-io/provenance/pull/1763)

### Bug Fixes

* Deprecate marker proposal transaction [#1797](https://github.com/provenance-io/provenance/issues/1797).

### Dependencies

- Bump `github.com/spf13/cobra` from 1.7.0 to 1.8.0 ([#1733](https://github.com/provenance-io/provenance/pull/1733))

---

## [v1.17.0](https://github.com/provenance-io/provenance/releases/tag/v1.17.0) - 2023-11-13
Expand Down
54 changes: 54 additions & 0 deletions cmd/provenanced/cmd/cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/cosmos/cosmos-sdk/x/genutil/client/cli"

"github.com/provenance-io/provenance/cmd/provenanced/cmd"
"github.com/provenance-io/provenance/testutil/assertions"
)

func TestInitCmd(t *testing.T) {
Expand All @@ -24,3 +25,56 @@ func TestInitCmd(t *testing.T) {
err := cmd.Execute(rootCmd)
require.NoError(t, err)
}

func TestGenAutoCompleteCmd(t *testing.T) {
home := t.TempDir()

tests := []struct {
name string
args []string
err string
}{
{
name: "failure - missing arg",
err: "accepts 1 arg(s), received 0",
},
{
name: "failure - too many args",
args: []string{"bash", "fish"},
err: "accepts 1 arg(s), received 2",
},
{
name: "failure - invalid shell type",
args: []string{"badshellname"},
err: "shell badshellname is not supported",
},
{
name: "success - works with bash",
args: []string{"bash"},
},
{
name: "success - works with zsh",
args: []string{"zsh"},
},
{
name: "success - works with fish",
args: []string{"fish"},
},
{
name: "success - works with powershell",
args: []string{"powershell"},
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
args := []string{"--home", home, "enable-cli-autocomplete"}
args = append(args, tc.args...)

rootCmd, _ := cmd.NewRootCmd(false)
rootCmd.SetArgs(args)
err := cmd.Execute(rootCmd)
assertions.AssertErrorValue(t, err, tc.err, "should have the correct output value")
})
}
}
13 changes: 13 additions & 0 deletions cmd/provenanced/cmd/export_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package cmd

// This file is in the cmd package (not cmd_test) so that it can expose
// some private keeper stuff for unit testing.

var (
// MakeDefaultMarket is a test-only exposure of makeDefaultMarket.
MakeDefaultMarket = makeDefaultMarket
// AddMarketsToAppState is a test-only exposure of addMarketsToAppState.
AddMarketsToAppState = addMarketsToAppState
// GetNextAvailableMarketID is a test-only exposure of getNextAvailableMarketID.
GetNextAvailableMarketID = getNextAvailableMarketID
)
235 changes: 235 additions & 0 deletions cmd/provenanced/cmd/genaccounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ import (
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
"github.com/cosmos/cosmos-sdk/x/genutil"
genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"

"github.com/provenance-io/provenance/x/exchange"
exchangecli "github.com/provenance-io/provenance/x/exchange/client/cli"
markercli "github.com/provenance-io/provenance/x/marker/client/cli"
markertypes "github.com/provenance-io/provenance/x/marker/types"
msgfeetypes "github.com/provenance-io/provenance/x/msgfees/types"
Expand All @@ -40,8 +43,55 @@ const (
flagEscrow = "escrow"
flagActivate = "activate"
flagFinalize = "finalize"

flagDenom = "denom"
)

// appStateUpdater is a function that makes modifications to an app-state.
// Use one in conjunction with updateGenesisFileRunE if your command only needs to parse
// some inputs to make additions or changes to app-state,
type appStateUpdater func(clientCtx client.Context, cmd *cobra.Command, args []string, appState map[string]json.RawMessage) error

// updateGenesisFile reads the existing genesis file, runs the app-state through the
// provided updater, then saves the updated genesis state over the existing genesis file.
func updateGenesisFile(cmd *cobra.Command, args []string, updater appStateUpdater) error {
clientCtx := client.GetClientContextFromCmd(cmd)
serverCtx := server.GetServerContextFromCmd(cmd)
config := serverCtx.Config
config.SetRoot(clientCtx.HomeDir)
genFile := config.GenesisFile()

// Get existing gen state
appState, genDoc, err := genutiltypes.GenesisStateFromGenFile(genFile)
if err != nil {
cmd.SilenceUsage = true
return fmt.Errorf("failed to read genesis file: %w", err)
}

err = updater(clientCtx, cmd, args, appState)
if err != nil {
return err
}

// None of the possible errors that might come after this will be helped by printing usage with them.
cmd.SilenceUsage = true

appStateJSON, err := json.Marshal(appState)
if err != nil {
return fmt.Errorf("failed to marshal application genesis state: %w", err)
}

genDoc.AppState = appStateJSON
return genutil.ExportGenesisFile(genDoc, genFile)
}

// updateGenesisFileRunE returns a cobra.Command.RunE function that runs updateGenesisFile using the provided updater.
func updateGenesisFileRunE(updater appStateUpdater) func(*cobra.Command, []string) error {
return func(cmd *cobra.Command, args []string) error {
return updateGenesisFile(cmd, args, updater)
}
}

// AddGenesisAccountCmd returns add-genesis-account cobra Command.
func AddGenesisAccountCmd(defaultNodeHome string) *cobra.Command {
cmd := &cobra.Command{
Expand Down Expand Up @@ -640,3 +690,188 @@ func checkMsgTypeValid(registry types.InterfaceRegistry, msgTypeURL string) erro
}
return err
}

// AddGenesisDefaultMarketCmd returns add-genesis-default-market cobra command.
func AddGenesisDefaultMarketCmd(defaultNodeHome string) *cobra.Command {
cmd := &cobra.Command{
Use: `add-genesis-default-market [--denom <denom>]

All base accounts already in the genesis file will be given all permissions on this market.
An error is returned if no such accounts are in the existing genesis file.

If --denom <denom> is not provided, the staking bond denom is used.
If no staking bond denom is defined either, then nhash is used.

This command is equivalent to the following command:
$ ` + version.AppName + ` add-genesis-custom-market \
--name 'Default <denom> Market' \
--create-ask 100<denom> --create-bid 100<denom> \
--seller-flat 500<denom> --buyer-flat 500<denom> \
--seller-ratios 20<denom>:1<denom> --buyer-ratios 20<denom>:1<denom> \
--accepting-orders --allow-user-settle \
--access-grants <all permissions to all known base accounts>

`,
Short: "Add a default market to the genesis file",
DisableFlagsInUseLine: true,
Args: cobra.NoArgs,
RunE: updateGenesisFileRunE(func(clientCtx client.Context, cmd *cobra.Command, _ []string, appState map[string]json.RawMessage) error {
// Printing usage with the errors from here won't actually help anyone.
cmd.SilenceUsage = true

// Identify the accounts that will get all the permissions.
var authGen authtypes.GenesisState
err := clientCtx.Codec.UnmarshalJSON(appState[authtypes.ModuleName], &authGen)
if err != nil {
return fmt.Errorf("could not extract auth genesis state: %w", err)
}
genAccts, err := authtypes.UnpackAccounts(authGen.Accounts)
if err != nil {
return fmt.Errorf("could not unpack genesis acounts: %w", err)
}
addrs := make([]string, 0, len(genAccts))
for _, acct := range genAccts {
// We specifically only want accounts that are base accounts (i.e. no markers or others).
// This should also include the validator accounts (that have already been added).
baseAcct, ok := acct.(*authtypes.BaseAccount)
if ok {
addrs = append(addrs, baseAcct.Address)
}
}
if len(addrs) == 0 {
return errors.New("genesis file must have one or more BaseAccount before a default market can be added")
}

// Identify the denom that we'll use for the fees.
feeDenom, err := cmd.Flags().GetString(flagDenom)
if err != nil {
return fmt.Errorf("error reading --%s value: %w", flagDenom, err)
}
if len(feeDenom) == 0 {
var stGen stakingtypes.GenesisState
err = clientCtx.Codec.UnmarshalJSON(appState[stakingtypes.ModuleName], &stGen)
if err == nil {
feeDenom = stGen.Params.BondDenom
}
}
if len(feeDenom) == 0 {
feeDenom = "nhash"
}
if err = sdk.ValidateDenom(feeDenom); err != nil {
return err
}

// Create the market and add it to the app state.
market := makeDefaultMarket(feeDenom, addrs)
return addMarketsToAppState(clientCtx, appState, market)
}),
}

cmd.Flags().String(flags.FlagHome, defaultNodeHome, "The application home directory")
cmd.Flags().String(flagDenom, "", "The fee denom for the market")
return cmd
}

// makeDefaultMarket creates the default market that uses the provided fee denom
// and gives all permissions to each of the provided addrs.
func makeDefaultMarket(feeDenom string, addrs []string) exchange.Market {
market := exchange.Market{
MarketDetails: exchange.MarketDetails{Name: "Default Market"},
AcceptingOrders: true,
AllowUserSettlement: true,
}

if len(feeDenom) > 0 {
creationFee := sdk.NewCoins(sdk.NewInt64Coin(feeDenom, 100))
settlementFlat := sdk.NewCoins(sdk.NewInt64Coin(feeDenom, 500))
settlementRatio := []exchange.FeeRatio{{Price: sdk.NewInt64Coin(feeDenom, 20), Fee: sdk.NewInt64Coin(feeDenom, 1)}}
market.MarketDetails.Name = fmt.Sprintf("Default %s Market", feeDenom)
market.FeeCreateAskFlat = creationFee
market.FeeCreateBidFlat = creationFee
market.FeeSellerSettlementFlat = settlementFlat
market.FeeSellerSettlementRatios = settlementRatio
market.FeeBuyerSettlementFlat = settlementFlat
market.FeeBuyerSettlementRatios = settlementRatio
}

for _, addr := range addrs {
market.AccessGrants = append(market.AccessGrants,
exchange.AccessGrant{Address: addr, Permissions: exchange.AllPermissions()})
}
return market
}

// AddGenesisCustomMarketCmd returns add-genesis-custom-market cobra command.
func AddGenesisCustomMarketCmd(defaultNodeHome string) *cobra.Command {
cmd := &cobra.Command{
Use: "add-genesis-custom-market",
Short: "Add a market to the genesis file",
RunE: updateGenesisFileRunE(func(clientCtx client.Context, cmd *cobra.Command, args []string, appState map[string]json.RawMessage) error {
msg, err := exchangecli.MakeMsgGovCreateMarket(clientCtx, cmd.Flags(), args)
if err != nil {
return err
}

// Now that we've read all the flags and stuff, no need to show usage with any errors anymore in here.
cmd.SilenceUsage = true
return addMarketsToAppState(clientCtx, appState, msg.Market)
}),
}

cmd.Flags().String(flags.FlagHome, defaultNodeHome, "The application home directory")
exchangecli.SetupCmdTxGovCreateMarket(cmd)
exchangecli.AddUseDetails(cmd, "If no <market id> is provided, the next available one will be used.")
return cmd
}

// addMarketsToAppState adds the given markets to the app state in the exchange module.
// If a provided market's MarketId is 0, the next available one will be identified and used.
func addMarketsToAppState(clientCtx client.Context, appState map[string]json.RawMessage, markets ...exchange.Market) error {
cdc := clientCtx.Codec
var exGenState exchange.GenesisState
if len(appState[exchange.ModuleName]) > 0 {
if err := cdc.UnmarshalJSON(appState[exchange.ModuleName], &exGenState); err != nil {
return fmt.Errorf("could not extract exchange genesis state: %w", err)
}
}

for _, market := range markets {
if err := market.Validate(); err != nil {
return err
}

if market.MarketId == 0 {
market.MarketId = getNextAvailableMarketID(exGenState)
if exGenState.LastMarketId < market.MarketId {
exGenState.LastMarketId = market.MarketId
}
}

exGenState.Markets = append(exGenState.Markets, market)
}

exGenStateBz, err := cdc.MarshalJSON(&exGenState)
if err != nil {
return fmt.Errorf("failed to marshal exchange genesis state: %w", err)
}
appState[exchange.ModuleName] = exGenStateBz
return nil
}

// getNextAvailableMarketID returns the next available market id given all the markets in the provided genesis state.
func getNextAvailableMarketID(exGenState exchange.GenesisState) uint32 {
if len(exGenState.Markets) == 0 {
return 1
}

marketIDsMap := make(map[uint32]bool, len(exGenState.Markets))
for _, market := range exGenState.Markets {
marketIDsMap[market.MarketId] = true
}

rv := uint32(1)
for marketIDsMap[rv] {
rv++
}
return rv
}
Loading
Loading